initial commit
This commit is contained in:
65
services/aiService.ts
Normal file
65
services/aiService.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
|
||||
import { GoogleGenAI } from "@google/genai";
|
||||
import { AiConfig } from "../types";
|
||||
|
||||
export class AiService {
|
||||
/**
|
||||
* 뉴스 기사들을 바탕으로 시장 심리 및 인사이트 분석
|
||||
*/
|
||||
static async analyzeNewsSentiment(config: AiConfig, newsHeadlines: string[]): Promise<string> {
|
||||
const prompt = `당신은 전문 주식 분석가입니다. 다음 뉴스 헤드라인들을 분석하여 시장의 심리(상승/하락/중립)와 투자자가 주목해야 할 핵심 포인트 3가지를 한국어로 요약해 주세요.
|
||||
|
||||
뉴스 헤드라인:
|
||||
${newsHeadlines.join('\n')}
|
||||
`;
|
||||
|
||||
if (config.providerType === 'gemini') {
|
||||
return this.callGemini(config.modelName, prompt);
|
||||
} else {
|
||||
return this.callOpenAiCompatible(config, prompt);
|
||||
}
|
||||
}
|
||||
|
||||
private static async callGemini(modelName: string, prompt: string): Promise<string> {
|
||||
try {
|
||||
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
||||
const response = await ai.models.generateContent({
|
||||
model: modelName || 'gemini-3-flash-preview',
|
||||
contents: prompt,
|
||||
});
|
||||
return response.text || "분석 결과를 생성할 수 없습니다.";
|
||||
} catch (error) {
|
||||
console.error("Gemini 분석 오류:", error);
|
||||
return `Gemini 분석 중 오류가 발생했습니다: ${error instanceof Error ? error.message : String(error)}`;
|
||||
}
|
||||
}
|
||||
|
||||
private static async callOpenAiCompatible(config: AiConfig, prompt: string): Promise<string> {
|
||||
if (!config.baseUrl) return "API 베이스 URL이 설정되지 않았습니다.";
|
||||
|
||||
try {
|
||||
const response = await fetch(`${config.baseUrl}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
// Ollama나 로컬 엔진은 보통 키가 필요 없거나 커스텀하게 처리하므로 일단 비워둡니다.
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: config.modelName,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
temperature: 0.7
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP 오류! 상태 코드: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.choices?.[0]?.message?.content || "분석 결과를 생성할 수 없습니다.";
|
||||
} catch (error) {
|
||||
console.error("OpenAI 호환 API 분석 오류:", error);
|
||||
return `AI 엔진(${config.name}) 분석 중 오류가 발생했습니다: ${error instanceof Error ? error.message : String(error)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
184
services/dbService.ts
Normal file
184
services/dbService.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
|
||||
import { TradeOrder, OrderType, MarketType, AutoTradeConfig, WatchlistGroup, ReservedOrder, StockTick } from '../types';
|
||||
|
||||
export interface HoldingItem {
|
||||
code: string;
|
||||
name: string;
|
||||
avgPrice: number;
|
||||
quantity: number;
|
||||
market: MarketType;
|
||||
}
|
||||
|
||||
export class DbService {
|
||||
private holdingsKey = 'batchukis_sqlite_holdings';
|
||||
private configsKey = 'batchukis_sqlite_configs';
|
||||
private watchlistGroupsKey = 'batchukis_sqlite_watchlist_groups';
|
||||
private reservedOrdersKey = 'batchukis_sqlite_reserved_orders';
|
||||
private ticksPrefix = 'batchukis_ticks_';
|
||||
|
||||
constructor() {
|
||||
this.initDatabase();
|
||||
}
|
||||
|
||||
private initDatabase() {
|
||||
if (!localStorage.getItem(this.holdingsKey)) {
|
||||
const initialHoldings: HoldingItem[] = [
|
||||
{ code: '005930', name: '삼성전자', avgPrice: 68500, quantity: 150, market: MarketType.DOMESTIC },
|
||||
{ code: 'AAPL', name: 'Apple Inc.', avgPrice: 175.20, quantity: 25, market: MarketType.OVERSEAS },
|
||||
];
|
||||
localStorage.setItem(this.holdingsKey, JSON.stringify(initialHoldings));
|
||||
}
|
||||
if (!localStorage.getItem(this.configsKey)) {
|
||||
localStorage.setItem(this.configsKey, JSON.stringify([]));
|
||||
}
|
||||
if (!localStorage.getItem(this.watchlistGroupsKey)) {
|
||||
const initialGroups: WatchlistGroup[] = [
|
||||
{ id: 'grp1', name: '핵심 우량주', codes: ['005930', '000660'], market: MarketType.DOMESTIC },
|
||||
{ id: 'grp2', name: 'AI 포트폴리오', codes: ['NVDA', 'TSLA'], market: MarketType.OVERSEAS },
|
||||
{ id: 'grp3', name: '미국 빅테크', codes: ['AAPL'], market: MarketType.OVERSEAS }
|
||||
];
|
||||
localStorage.setItem(this.watchlistGroupsKey, JSON.stringify(initialGroups));
|
||||
}
|
||||
if (!localStorage.getItem(this.reservedOrdersKey)) {
|
||||
localStorage.setItem(this.reservedOrdersKey, JSON.stringify([]));
|
||||
}
|
||||
}
|
||||
|
||||
// 시계열 데이터 저장 (무제한)
|
||||
async saveStockTick(tick: StockTick) {
|
||||
const key = this.ticksPrefix + tick.code;
|
||||
const existing = localStorage.getItem(key);
|
||||
const ticks: StockTick[] = existing ? JSON.parse(existing) : [];
|
||||
ticks.push(tick);
|
||||
localStorage.setItem(key, JSON.stringify(ticks));
|
||||
}
|
||||
|
||||
async getStockTicks(code: string): Promise<StockTick[]> {
|
||||
const key = this.ticksPrefix + code;
|
||||
const data = localStorage.getItem(key);
|
||||
return data ? JSON.parse(data) : [];
|
||||
}
|
||||
|
||||
async getHoldings(): Promise<HoldingItem[]> {
|
||||
const data = localStorage.getItem(this.holdingsKey);
|
||||
return data ? JSON.parse(data) : [];
|
||||
}
|
||||
|
||||
async syncOrderToHolding(order: TradeOrder) {
|
||||
const holdings = await this.getHoldings();
|
||||
const existingIdx = holdings.findIndex(h => h.code === order.stockCode);
|
||||
|
||||
if (order.type === OrderType.BUY) {
|
||||
if (existingIdx > -1) {
|
||||
const h = holdings[existingIdx];
|
||||
const newQty = h.quantity + order.quantity;
|
||||
const newAvg = ((h.avgPrice * h.quantity) + (order.price * order.quantity)) / newQty;
|
||||
holdings[existingIdx] = { ...h, quantity: newQty, avgPrice: newAvg };
|
||||
} else {
|
||||
holdings.push({
|
||||
code: order.stockCode,
|
||||
name: order.stockName,
|
||||
avgPrice: order.price,
|
||||
quantity: order.quantity,
|
||||
market: order.stockCode.length > 6 ? MarketType.OVERSEAS : MarketType.DOMESTIC
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (existingIdx > -1) {
|
||||
holdings[existingIdx].quantity -= order.quantity;
|
||||
if (holdings[existingIdx].quantity <= 0) {
|
||||
holdings.splice(existingIdx, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
localStorage.setItem(this.holdingsKey, JSON.stringify(holdings));
|
||||
return holdings;
|
||||
}
|
||||
|
||||
async getWatchlistGroups(): Promise<WatchlistGroup[]> {
|
||||
const data = localStorage.getItem(this.watchlistGroupsKey);
|
||||
return data ? JSON.parse(data) : [];
|
||||
}
|
||||
|
||||
async saveWatchlistGroup(group: WatchlistGroup) {
|
||||
const groups = await this.getWatchlistGroups();
|
||||
groups.push(group);
|
||||
localStorage.setItem(this.watchlistGroupsKey, JSON.stringify(groups));
|
||||
}
|
||||
|
||||
async updateWatchlistGroup(group: WatchlistGroup) {
|
||||
const groups = await this.getWatchlistGroups();
|
||||
const idx = groups.findIndex(g => g.id === group.id);
|
||||
if (idx > -1) {
|
||||
groups[idx] = group;
|
||||
localStorage.setItem(this.watchlistGroupsKey, JSON.stringify(groups));
|
||||
}
|
||||
}
|
||||
|
||||
async deleteWatchlistGroup(id: string) {
|
||||
const groups = await this.getWatchlistGroups();
|
||||
const filtered = groups.filter(g => g.id !== id);
|
||||
localStorage.setItem(this.watchlistGroupsKey, JSON.stringify(filtered));
|
||||
}
|
||||
|
||||
async getAutoConfigs(): Promise<AutoTradeConfig[]> {
|
||||
const data = localStorage.getItem(this.configsKey);
|
||||
return data ? JSON.parse(data) : [];
|
||||
}
|
||||
|
||||
async saveAutoConfig(config: AutoTradeConfig) {
|
||||
const configs = await this.getAutoConfigs();
|
||||
configs.push(config);
|
||||
localStorage.setItem(this.configsKey, JSON.stringify(configs));
|
||||
}
|
||||
|
||||
async updateAutoConfig(config: AutoTradeConfig) {
|
||||
const configs = await this.getAutoConfigs();
|
||||
const idx = configs.findIndex(c => c.id === config.id);
|
||||
if (idx > -1) {
|
||||
configs[idx] = config;
|
||||
localStorage.setItem(this.configsKey, JSON.stringify(configs));
|
||||
}
|
||||
}
|
||||
|
||||
async deleteAutoConfig(id: string) {
|
||||
const configs = await this.getAutoConfigs();
|
||||
const filtered = configs.filter(c => c.id !== id);
|
||||
localStorage.setItem(this.configsKey, JSON.stringify(filtered));
|
||||
}
|
||||
|
||||
async getReservedOrders(): Promise<ReservedOrder[]> {
|
||||
const data = localStorage.getItem(this.reservedOrdersKey);
|
||||
return data ? JSON.parse(data) : [];
|
||||
}
|
||||
|
||||
async saveReservedOrder(order: ReservedOrder) {
|
||||
const orders = await this.getReservedOrders();
|
||||
orders.push(order);
|
||||
localStorage.setItem(this.reservedOrdersKey, JSON.stringify(orders));
|
||||
}
|
||||
|
||||
async updateReservedOrder(order: ReservedOrder) {
|
||||
const orders = await this.getReservedOrders();
|
||||
const idx = orders.findIndex(o => o.id === order.id);
|
||||
if (idx > -1) {
|
||||
orders[idx] = order;
|
||||
localStorage.setItem(this.reservedOrdersKey, JSON.stringify(orders));
|
||||
}
|
||||
}
|
||||
|
||||
async deleteReservedOrder(id: string) {
|
||||
const orders = await this.getReservedOrders();
|
||||
const filtered = orders.filter(o => o.id !== id);
|
||||
localStorage.setItem(this.reservedOrdersKey, JSON.stringify(filtered));
|
||||
}
|
||||
|
||||
async getAccountSummary() {
|
||||
const holdings = await this.getHoldings();
|
||||
const totalEval = holdings.reduce((acc, h) => acc + (h.avgPrice * h.quantity), 0);
|
||||
return {
|
||||
totalAssets: totalEval + 45800000,
|
||||
buyingPower: 45800000
|
||||
};
|
||||
}
|
||||
}
|
||||
33
services/geminiService.ts
Normal file
33
services/geminiService.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
|
||||
import { GoogleGenAI } from "@google/genai";
|
||||
|
||||
export async function analyzeMarketSentiment(news: string[]) {
|
||||
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
||||
const prompt = `Based on the following news headlines, provide a concise market sentiment summary (Bullish/Bearish/Neutral) and 3 key takeaways for a stock trader. News: ${news.join('. ')}`;
|
||||
|
||||
try {
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-3-flash-preview',
|
||||
contents: prompt,
|
||||
});
|
||||
return response.text;
|
||||
} catch (error) {
|
||||
console.error("Gemini Error:", error);
|
||||
return "AI insight currently unavailable.";
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStockInsight(stockName: string, recentTrend: string) {
|
||||
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
||||
const prompt = `Analyze the stock ${stockName} which has been ${recentTrend}. Suggest a potential strategy for an automated trading bot (e.g., Trailing Stop percentage, Accumulation frequency).`;
|
||||
|
||||
try {
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-3-pro-preview',
|
||||
contents: prompt,
|
||||
});
|
||||
return response.text;
|
||||
} catch (error) {
|
||||
return "Detailed analysis unavailable at this moment.";
|
||||
}
|
||||
}
|
||||
61
services/kisService.ts
Normal file
61
services/kisService.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
|
||||
import { ApiSettings, MarketType, OrderType, StockItem } from '../types';
|
||||
|
||||
/**
|
||||
* Korea Investment & Securities (KIS) Open API Service
|
||||
*/
|
||||
export class KisService {
|
||||
private settings: ApiSettings;
|
||||
private accessToken: string | null = null;
|
||||
|
||||
constructor(settings: ApiSettings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
async issueAccessToken() {
|
||||
this.accessToken = "mock_token_" + Math.random().toString(36).substr(2);
|
||||
return this.accessToken;
|
||||
}
|
||||
|
||||
async inquirePrice(code: string): Promise<number> {
|
||||
const basePrice = code.startsWith('0') ? 70000 : 150;
|
||||
return Math.floor(basePrice + Math.random() * 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 서버로부터 전체 종목 마스터 리스트를 가져오는 Mock 함수
|
||||
*/
|
||||
async fetchMasterStocks(market: MarketType): Promise<StockItem[]> {
|
||||
console.log(`KIS: Fetching master stocks for ${market}...`);
|
||||
// 백엔드 구현 전까지는 시뮬레이션 데이터를 반환합니다.
|
||||
if (market === MarketType.DOMESTIC) {
|
||||
return [
|
||||
{ code: '005930', name: '삼성전자', price: 73200, change: 800, changePercent: 1.1, market: MarketType.DOMESTIC, volume: 15234000, aiScoreBuy: 85, aiScoreSell: 20, themes: ['반도체', 'AI', '스마트폰'] },
|
||||
{ code: '000660', name: 'SK하이닉스', price: 124500, change: -1200, changePercent: -0.96, market: MarketType.DOMESTIC, volume: 2100000, aiScoreBuy: 65, aiScoreSell: 45, themes: ['반도체', 'HBM'] },
|
||||
{ code: '035420', name: 'NAVER', price: 215000, change: 4500, changePercent: 2.14, market: MarketType.DOMESTIC, volume: 850000, aiScoreBuy: 72, aiScoreSell: 30, themes: ['플랫폼', 'AI'] },
|
||||
{ code: '035720', name: '카카오', price: 58200, change: 300, changePercent: 0.52, market: MarketType.DOMESTIC, volume: 1200000, aiScoreBuy: 50, aiScoreSell: 50, themes: ['플랫폼', '모빌리티'] },
|
||||
{ code: '005380', name: '현대차', price: 245000, change: 2000, changePercent: 0.82, market: MarketType.DOMESTIC, volume: 450000, aiScoreBuy: 78, aiScoreSell: 25, themes: ['자동차', '전기차'] },
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
{ code: 'AAPL', name: 'Apple Inc.', price: 189.43, change: 1.25, changePercent: 0.66, market: MarketType.OVERSEAS, volume: 45000000, aiScoreBuy: 90, aiScoreSell: 15, themes: ['빅테크', '스마트폰'] },
|
||||
{ code: 'TSLA', name: 'Tesla Inc.', price: 234.12, change: -4.50, changePercent: -1.89, market: MarketType.OVERSEAS, volume: 110000000, aiScoreBuy: 40, aiScoreSell: 75, themes: ['전기차', '자율주행'] },
|
||||
{ code: 'NVDA', name: 'NVIDIA Corp.', price: 485.12, change: 12.30, changePercent: 2.6, market: MarketType.OVERSEAS, volume: 32000000, aiScoreBuy: 95, aiScoreSell: 10, themes: ['반도체', 'AI'] },
|
||||
{ code: 'MSFT', name: 'Microsoft Corp.', price: 402.12, change: 3.45, changePercent: 0.86, market: MarketType.OVERSEAS, volume: 22000000, aiScoreBuy: 88, aiScoreSell: 12, themes: ['소프트웨어', 'AI'] },
|
||||
{ code: 'GOOGL', name: 'Alphabet Inc.', price: 145.12, change: 0.55, changePercent: 0.38, market: MarketType.OVERSEAS, volume: 18000000, aiScoreBuy: 75, aiScoreSell: 20, themes: ['검색', 'AI'] },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
async orderCash(code: string, type: OrderType, quantity: number, price: number = 0) {
|
||||
return { success: true, orderId: "ORD-" + Math.random().toString(36).substr(2, 9) };
|
||||
}
|
||||
|
||||
async orderOverseas(code: string, type: OrderType, quantity: number, price: number) {
|
||||
return { success: true, orderId: "OS-ORD-" + Math.random().toString(36).substr(2, 9) };
|
||||
}
|
||||
|
||||
async inquireBalance() {
|
||||
return { output1: [], output2: { tot_evlu_amt: "124500000", nass_amt: "45800000" } };
|
||||
}
|
||||
}
|
||||
38
services/naverService.ts
Normal file
38
services/naverService.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
|
||||
import { ApiSettings, NewsItem } from '../types';
|
||||
|
||||
export class NaverService {
|
||||
private settings: ApiSettings;
|
||||
|
||||
constructor(settings: ApiSettings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
async fetchNews(query: string = "주식"): Promise<NewsItem[]> {
|
||||
if (!this.settings.useNaverNews || !this.settings.naverClientId) {
|
||||
console.log("Naver News: Service disabled or configuration missing.");
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(`Naver News: Fetching for "${query}"...`);
|
||||
|
||||
// 실제 API 연동 시 백엔드 프록시 호출 권장
|
||||
// const response = await fetch(`/api/naver-news?query=${encodeURIComponent(query)}`);
|
||||
// return await response.json();
|
||||
|
||||
return [
|
||||
{
|
||||
title: `[속보] ${query} 관련 글로벌 거시 경제 영향 분석 보고서`,
|
||||
description: "현재 시장 상황에서 해당 섹터의 성장이 두드러지고 있으며 기관 투자자들의 매수세가 이어지고 있습니다.",
|
||||
link: "https://news.naver.com",
|
||||
pubDate: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
title: `${query} 실시간 수급 현황 및 외인 매수세 유입`,
|
||||
description: "외국인 투자자들이 3거래일 연속 순매수를 기록하며 주가 상승을 견인하고 있습니다.",
|
||||
link: "https://news.naver.com",
|
||||
pubDate: new Date().toISOString()
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
49
services/telegramService.ts
Normal file
49
services/telegramService.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
|
||||
import { ApiSettings } from '../types';
|
||||
|
||||
/**
|
||||
* Telegram Notification Client Service
|
||||
* 프론트엔드에서는 백엔드에 알림 요청만 보내며,
|
||||
* 실제 봇 호출 및 메시지 구성은 서버(백엔드)에서 안전하게 처리합니다.
|
||||
*/
|
||||
export class TelegramService {
|
||||
private settings: ApiSettings;
|
||||
|
||||
constructor(settings: ApiSettings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 백엔드 알림 API를 호출합니다.
|
||||
* 백엔드 엔진은 사용자가 브라우저를 닫아도 독립적으로 이 로직을 수행합니다.
|
||||
*/
|
||||
async sendMessage(message: string): Promise<boolean> {
|
||||
if (!this.settings.useTelegram) return false;
|
||||
|
||||
console.log(`[Frontend] 요청된 알림 메시지: ${message}`);
|
||||
|
||||
try {
|
||||
// 실제 환경에서는 백엔드 API 호출:
|
||||
// const response = await fetch('/api/notify/send', {
|
||||
// method: 'POST',
|
||||
// headers: { 'Content-Type': 'application/json' },
|
||||
// body: JSON.stringify({ message })
|
||||
// });
|
||||
// return response.ok;
|
||||
|
||||
return true; // 데모용 성공 반환
|
||||
} catch (e) {
|
||||
console.error("Telegram notification request failed", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 설정 페이지에서 알림 연결 테스트용
|
||||
*/
|
||||
async testConnection(token: string, chatId: string): Promise<{success: boolean, msg: string}> {
|
||||
console.log("Telegram: Testing connection through backend...");
|
||||
// 백엔드 /api/notify/test 호출 로직
|
||||
return { success: true, msg: "테스트 메시지가 발송되었습니다." };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user