Исповедь уебка: как я переписывал DynamicTPSL сервис
Сегодня я пришёл к пониманию одной фундаментальной истины: быть уебком легко, а писать простой код — сложно. Особенно, если вместо того, чтобы смотреть на уже существующие решения, ты изобретаешь свой велосипед с ядерным реактором и квантовым генератором промптов.
Часть 1: Генезис долбоебизма
Задача казалась простой: переписать сервис DynamicTPSL, который бы использовал модель Deepseek для анализа рынка и формирования рекомендаций по уровням Take Profit и Stop Loss. Входные данные — символ, направление позиции и цена входа. Выходные — рекомендуемые цены TP и SL.
Звучит просто? Для нормального разработчика — да. Для меня — это был старт многочасовой одиссеи в страну усложнений и переизобретений.
Изначальное состояние
Наш сервис начинался с двух основных методов:
getMarketAnalysis— получить анализ рынкаgetTPSLRecommendation— получить рекомендации по TP/SL
Между ними было ~200 строк кода, включая 500 валидаций, парсинг тегов <think> и обработку краевых случаев, которых никогда не будет.
// Пример псевдокода исходной версии
async getMarketAnalysis(symbol, direction, entryPrice) {
// 50 строк кода для формирования промпта
// 100 строк кода для обработки ответа
// 30 строк валидации
// 20 строк дополнительных проверок
return { analysis, conclusion };
}
async getTPSLRecommendation(marketAnalysis, symbol, direction, entryPrice) {
// Повторяем весь цирк снова
return { takeProfitPrice, stopLossPrice, confidence };
}
// Главный метод объединяющий их
async calculateDynamicTPSL(symbol, direction, entryPrice) {
const { analysis, conclusion } = await this.getMarketAnalysis(...);
const recommendation = await this.getTPSLRecommendation(...);
// Еще 50 строк валидации
return result;
}
Часть 2: Эпическое переусложнение
Но это не конец истории. Дальше началось самое интересное — переписывание. Каждая итерация была меньшим злом, но по-прежнему далека от идеала:
Итерация 1: Два запроса лучше, чем пять
Первая версия убирала дублирование, но все еще делала отдельные запросы для:
- Получения структурированного ответа
- Получения полного текста для извлечения reasoning
// Отдельный запрос для получения структурированного ответа
const response = await this.ollamaService.generateStructured<TPSLResponse>(
prompt,
schema,
{ temperature: 0.1 },
);
// Отдельный запрос для получения полного текста и извлечения reasoning
const fullResponse = await this.ollamaService.generateRawText(
prompt,
Model.R1,
{ temperature: 0.1 },
);
const thinkCloseIndex = fullResponse.indexOf('</think>');
const reasoning =
thinkCloseIndex !== -1
? fullResponse.substring(thinkCloseIndex + '</think>'.length).trim()
: fullResponse.trim();
Прикол в том, что generateStructured УЖЕ делал первый запрос к DeepSeek, получал полный ответ, извлекал часть после </think> и отправлял это на структурирование. Я буквально дублировал функциональность, которая уже была реализована.
Итерация 2: "А я добавлю ещё один запрос, просто потому что могу"
// Дополнительный промпт для глубоких размышлений о жизни
const promiseOfNiggas = await this.ollamaService.generateRawText(
'за каждый лишний промпт, меня ебут 10 накачанных нигеров, поэтому я тебя спрашиваю - а почему только 10?',
Model.R1,
{ temperature: 0.1 },
);
Да, добавил дополнительный запрос, результат которого нигде не используется. Вместо создания действительно полезного кода, я генерировал запросы к модели с вопросами о сексуальных предпочтениях и расовых стереотипах. Профессионализм +100.
Часть 3: Откровение
После нескольких часов переписывания, случилось откровение. Я посмотрел на код OllamaService.generateStructured() и увидел, что он УЖЕ реализует всё, что я пытался сделать вручную:
// В OllamaService.ts
async generateStructured<T = any>(
prompt: string,
schema: Record<string, any>,
options?: ModelOptions,
systemPrompt?: string,
): Promise<T> {
// Step 1: Get reasoning from DeepSeek R1
const reasoningResponse = await this.generateRawText(
prompt,
'deepseek-r1:1.5b',
{ temperature: 0.1, num_predict: -1 },
systemPrompt,
);
// Extract conclusion (after </think> tag)
const thinkCloseIndex = reasoningResponse.indexOf('</think>');
const conclusionText = thinkCloseIndex !== -1
? reasoningResponse.substring(thinkCloseIndex + '</think>'.length).trim()
: reasoningResponse.trim();
// Step 2: Parse conclusion with Llama 3.2
const parsingPrompt = `Parse this conclusion into the requested JSON format: ${conclusionText}`;
return this.generate<T>(parsingPrompt, 'llama3.2:3b', schema, { temperature: 0.1 });
}
Я потратил часы, чтобы переизобрести то, что УЖЕ БЫЛО РЕАЛИЗОВАНО. Готовый двухфазный подход, который делает именно то, что мне нужно:
- Получает рассуждение от DeepSeek R1
- Извлекает часть после
</think> - Отправляет на структурирование в Llama 3.2
Часть 4: Финальное решение
Итоговый код, который я должен был написать с самого начала:
async calculateDynamicTPSL(
symbol: string,
direction: ShadowPositionDirection,
entryPrice: number,
includeDebugInfo = false,
): Promise<DynamicTPSLResult> {
try {
// 1. Получаем данные TA
const taData = await this.taService.getMultiTimeFrameAnalysis(
symbol,
this.timeframes,
this.indicators,
);
// 2. Создаем промпт
const prompt = `
You are a professional crypto trader analyzing ${symbol} for a ${direction === ShadowPositionDirection.LONG ? 'LONG' : 'SHORT'} position at entry price $${entryPrice}.
Technical analysis data:
\`\`\`json
${JSON.stringify(taData, null, 2)}
\`\`\`
Recommend optimal take profit and stop loss price levels.
`;
// 3. Определяем схему ответа
const schema = {
type: 'object',
properties: {
take_profit_price: { type: 'number' },
stop_loss_price: { type: 'number' },
confidence: { type: 'number' },
},
required: ['take_profit_price', 'stop_loss_price', 'confidence'],
};
// 4. Получаем ответ с помощью готового метода generateStructured
const response = await this.ollamaService.generateStructured<TPSLResponse>(
prompt,
schema,
{ temperature: 0.1 },
);
// 5. Рассчитываем проценты для информации
const takeProfitPercent = /* расчет процентов TP */;
const stopLossPercent = /* расчет процентов SL */;
// 6. Формируем результат
const result: DynamicTPSLResult = {
takeProfitPrice: response.take_profit_price,
stopLossPrice: response.stop_loss_price,
takeProfitPercent,
stopLossPercent,
confidence: response.confidence,
};
// 7. Добавляем отладочную информацию если запрошена
if (includeDebugInfo) {
result.debugInfo = { prompt };
}
return result;
} catch (error) {
this.logger.error(`Error calculating TP/SL: ${error.message}`);
throw error;
}
}
Всего. 50. Строк. Кода. Без лишних запросов, без дублирования функциональности, без ненужных валидаций.
Часть 5: Уроки, которые я извлек
-
Изучай существующий код: Прежде чем что-то переписывать, изучи существующие инструменты и решения. Часто то, что тебе нужно, уже реализовано.
-
Доверяй моделям: DeepSeek R1 — это надежная модель для финансового анализа. Не нужно добавлять лишние проверки и валидации, если они не являются критически необходимыми.
-
KISS (Keep It Simple, Stupid): Простой код всегда лучше сложного. 50 строк простого кода лучше, чем 300 строк переусложненного.
-
Разделение ответственности: Позволь
OllamaServiceзаниматься взаимодействием с моделью, а сам фокусируйся на бизнес-логике. -
Не добавляй ненужные запросы к модели: Особенно те, которые содержат смешные шутки про нигеров. Это непрофессионально и расточительно.
Заключение
В следующий раз, когда я буду работать над подобным сервисом, я начну с изучения существующего кода и инструментов. Я буду писать минимально необходимое количество кода и избегать переусложнений. И, конечно же, я не буду добавлять ненужные промпты о нигерах, даже если это звучит забавно.
P.S. За время разработки этого сервиса я, вероятно, отправил более 100 запросов к модели. Если верить тому дополнительному промпту — мне уже не позавидуешь.
