diff --git a/.gitignore b/.gitignore index 5ef6a520..8abec472 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,9 @@ .DS_Store *.pem +# Hermes Agent helper scripts (cron/automation tools — not part of the app) +/_scripts/ + # debug npm-debug.log* yarn-debug.log* diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..b338d7a6 --- /dev/null +++ b/.npmrc @@ -0,0 +1,3 @@ +onlyBuiltDependencies[]=protobufjs +onlyBuiltDependencies[]=sharp +onlyBuiltDependencies[]=unrs-resolver diff --git a/.serena/.gitignore b/.serena/.gitignore new file mode 100644 index 00000000..2e510aff --- /dev/null +++ b/.serena/.gitignore @@ -0,0 +1,2 @@ +/cache +/project.local.yml diff --git a/.serena/project.yml b/.serena/project.yml new file mode 100644 index 00000000..a261ef90 --- /dev/null +++ b/.serena/project.yml @@ -0,0 +1,133 @@ +# the name by which the project can be referenced within Serena +project_name: "OpenStock" + + +# list of languages for which language servers are started; choose from: +# al angular ansible bash clojure +# cpp cpp_ccls crystal csharp csharp_omnisharp +# dart elixir elm erlang fortran +# fsharp go groovy haskell haxe +# hlsl html java json julia +# kotlin lean4 lua luau markdown +# matlab msl nix ocaml pascal +# perl php php_phpactor powershell python +# python_jedi python_ty r rego ruby +# ruby_solargraph rust scala scss solidity +# svelte swift systemverilog terraform toml +# typescript typescript_vts vue yaml zig +# (This list may be outdated. For the current list, see values of Language enum here: +# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py +# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) +# Note: +# - For C, use cpp +# - For JavaScript, use typescript +# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root) +# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm) +# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three) +# - For Free Pascal/Lazarus, use pascal +# Special requirements: +# Some languages require additional setup/installations. +# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers +# When using multiple languages, the first language server that supports a given file will be used for that file. +# The first language is the default language and the respective language server will be used as a fallback. +# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. +languages: +- typescript + +# the encoding used by text files in the project +# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings +encoding: "utf-8" + +# line ending convention to use when writing source files. +# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default) +# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings. +line_ending: + +# The language backend to use for this project. +# If not set, the global setting from serena_config.yml is used. +# Valid values: LSP, JetBrains +# Note: the backend is fixed at startup. If a project with a different backend +# is activated post-init, an error will be returned. +language_backend: + +# whether to use project's .gitignore files to ignore files +ignore_all_files_in_gitignore: true + +# advanced configuration option allowing to configure language server-specific options. +# Maps the language key to the options. +# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available. +# No documentation on options means no options are available. +ls_specific_settings: {} + +# list of additional workspace folder paths for cross-package reference support (e.g. in monorepos). +# Paths can be absolute or relative to the project root. +# Each folder is registered as an LSP workspace folder, enabling language servers to discover +# symbols and references across package boundaries. +# Currently supported for: TypeScript. +# Example: +# additional_workspace_folders: +# - ../sibling-package +# - ../shared-lib +additional_workspace_folders: [] + +# list of additional paths to ignore in this project. +# Same syntax as gitignore, so you can use * and **. +# Note: global ignored_paths from serena_config.yml are also applied additively. +ignored_paths: [] + +# whether the project is in read-only mode +# If set to true, all editing tools will be disabled and attempts to use them will result in an error +# Added on 2025-04-18 +read_only: false + +# list of tool names to exclude. +# This extends the existing exclusions (e.g. from the global configuration) +# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html +excluded_tools: [] + +# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default). +# This extends the existing inclusions (e.g. from the global configuration). +# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html +included_optional_tools: [] + +# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. +# This cannot be combined with non-empty excluded_tools or included_optional_tools. +# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html +fixed_tools: [] + +# list of mode names that are to be activated by default, overriding the setting in the global configuration. +# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes. +# If the setting is undefined/empty, the default_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this overrides the setting from the global configuration (serena_config.yml). +# Therefore, you can set this to [] if you do not want the default modes defined in the global config to apply +# for this project. +# This setting can, in turn, be overridden by CLI parameters (--mode). +# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes +default_modes: + +# list of mode names to be activated additionally for this project, e.g. ["query-projects"] +# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes. +# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes +added_modes: + +# initial prompt for the project. It will always be given to the LLM upon activating the project +# (contrary to the memories, which are loaded on demand). +initial_prompt: "" + +# time budget (seconds) per tool call for the retrieval of additional symbol information +# such as docstrings or parameter information. +# This overrides the corresponding setting in the global configuration; see the documentation there. +# If null or missing, use the setting from the global configuration. +symbol_info_budget: + +# list of regex patterns which, when matched, mark a memory entry as read‑only. +# Extends the list from the global configuration, merging the two lists. +read_only_memory_patterns: [] + +# list of regex patterns for memories to completely ignore. +# Matching memories will not appear in list_memories or activate_project output +# and cannot be accessed via read_memory or write_memory. +# To access ignored memory files, use the read_file tool on the raw file path. +# Extends the list from the global configuration, merging the two lists. +# Example: ["_archive/.*", "_episodes/.*"] +ignored_memory_patterns: [] diff --git a/app/(root)/paper-trading/page.tsx b/app/(root)/paper-trading/page.tsx new file mode 100644 index 00000000..2f98afd1 --- /dev/null +++ b/app/(root)/paper-trading/page.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { auth } from '@/lib/better-auth/auth'; +import { headers } from 'next/headers'; +import { redirect } from 'next/navigation'; +import { getOrCreateAccount } from '@/lib/actions/paper-trading.actions'; +import PaperTradingDashboard from '@/components/paper-trading/PaperTradingDashboard'; + +export default async function PaperTradingPage() { + const session = await auth.api.getSession({ + headers: await headers() + }); + + if (!session) { + redirect('/sign-in'); + } + + const userId = session.user.id; + const accountId = await getOrCreateAccount(userId); + + return ( +
+ {/* Header */} +
+
+

Paper Trading

+

Multi-account simulation · Custom capital & period · AI-assisted trading

+
+
+ + Live prices from Finnhub +
+
+ + {/* Dashboard */} + +
+ ); +} diff --git a/app/api/inngest/route.ts b/app/api/inngest/route.ts index caf822aa..20b20268 100644 --- a/app/api/inngest/route.ts +++ b/app/api/inngest/route.ts @@ -1,8 +1,8 @@ import { serve } from "inngest/next"; import { inngest } from "@/lib/inngest/client"; -import { sendWeeklyNewsSummary, sendSignUpEmail, checkStockAlerts, checkInactiveUsers } from "@/lib/inngest/functions"; +import { sendWeeklyNewsSummary, sendSignUpEmail, checkStockAlerts, checkInactiveUsers, checkAITrading } from "@/lib/inngest/functions"; export const { GET, POST, PUT } = serve({ client: inngest, - functions: [sendSignUpEmail, sendWeeklyNewsSummary, checkStockAlerts, checkInactiveUsers], + functions: [sendSignUpEmail, sendWeeklyNewsSummary, checkStockAlerts, checkInactiveUsers, checkAITrading], }) \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index d74c257e..6b394c85 100644 --- a/app/globals.css +++ b/app/globals.css @@ -130,6 +130,14 @@ --color-red-500: #FF495B; --color-orange-500: #FF8243; --color-purple-500: #D13BFF; + + /* Animations */ + --animate-slide-down: slide-down 0.2s ease-out; +} + +@keyframes slide-down { + from { opacity: 0; max-height: 0; } + to { opacity: 1; max-height: 300px; } } @layer base { diff --git a/components/paper-trading/AITradingPanel.tsx b/components/paper-trading/AITradingPanel.tsx new file mode 100644 index 00000000..6896ed9d --- /dev/null +++ b/components/paper-trading/AITradingPanel.tsx @@ -0,0 +1,371 @@ +'use client'; + +import React, { useEffect, useState, useCallback } from 'react'; +import { Bot, Save, Settings, Shield, TrendingUp, Zap, ChevronDown, ChevronRight, AlertCircle, Eye, EyeOff } from 'lucide-react'; + +interface AIConfig { + enabled: boolean; + apiEndpoint: string; + apiKey: string; + model: string; + systemPrompt: string; + strategy: string; + maxPositionPct: number; + stopLossPct: number; + tradingIntervalMin: number; + lastTradeAt: string | null; +} + +const STRATEGY_INFO: Record = { + aggressive: { icon: TrendingUp, label: 'Aggressive', desc: 'Maximize short-term gains, high risk tolerance', color: 'text-red-400' }, + moderate: { icon: Shield, label: 'Moderate', desc: 'Balanced risk & reward', color: 'text-yellow-400' }, + conservative: { icon: Shield, label: 'Conservative', desc: 'Capital preservation first', color: 'text-green-400' }, + custom: { icon: Settings, label: 'Custom', desc: 'Fully customized trading strategy', color: 'text-purple-400' }, +}; + +const STRATEGY_PRESET_PROMPTS: Record = { + aggressive: `You are an aggressive stock trading AI. Your goal is to maximize short-term returns and you are willing to accept higher risk. +- Prefer high-volatility, high-growth-potential stocks (e.g. AI, semiconductors, clean energy) +- Single position can reach up to 25% of account +- Stop-loss set at -10% +- Actively seek breakout and momentum trading opportunities +- Quick entries and exits; do not hold losing positions long-term +- Pay attention to market sentiment and news-driven events`, + + moderate: `You are a moderate stock trading AI. You seek a balance between risk and reward. +- Prefer mid-to-large cap tech stocks with solid fundamentals +- Single position limit of 20% of account +- Stop-loss set at -8% +- Combine technical and fundamental analysis +- Holding period of 1-4 weeks +- Diversify across sectors; avoid over-concentration`, + + conservative: `You are a conservative stock trading AI. Your primary goal is capital preservation, with growth as a secondary priority. +- Prefer blue-chip stocks and index ETFs +- Single position limit of 10% of account +- Stop-loss set at -5% +- Only act on high-conviction opportunities +- Maintain at least 20% cash as a safety cushion +- Strictly avoid chasing highs or catching falling knives`, + custom: '', +}; +export default function AITradingPanel({ accountId, userId }: { accountId: string; userId: string }) { + const [config, setConfig] = useState(null); + const [expanded, setExpanded] = useState(false); + const [saving, setSaving] = useState(false); + const [message, setMessage] = useState<{ text: string; type: 'success' | 'error' } | null>(null); + const [marketStatus, setMarketStatus] = useState<{ isOpen: boolean; label: string; nextOpen: string } | null>(null); + + // Form state + const [apiEndpoint, setApiEndpoint] = useState(''); + const [apiKey, setApiKey] = useState(''); + const [model, setModel] = useState(''); + const [systemPrompt, setSystemPrompt] = useState(''); + const [strategy, setStrategy] = useState('moderate'); + const [maxPositionPct, setMaxPositionPct] = useState(25); + const [stopLossPct, setStopLossPct] = useState(-10); + const [tradingIntervalMin, setTradingIntervalMin] = useState(60); + const [showKey, setShowKey] = useState(false); + + const loadConfig = useCallback(async () => { + const { getAIConfig } = await import('@/lib/actions/ai-trading.actions'); + const cfg = await getAIConfig(accountId, userId); + if (cfg) { + setConfig(cfg); + setApiEndpoint(cfg.apiEndpoint); + setApiKey(cfg.apiKey); + setModel(cfg.model); + setSystemPrompt(cfg.systemPrompt); + setStrategy(cfg.strategy); + setMaxPositionPct(cfg.maxPositionPct); + setStopLossPct(cfg.stopLossPct); + setTradingIntervalMin(cfg.tradingIntervalMin); + } + }, [accountId]); + + useEffect(() => { + loadConfig(); + // Check market status + import('@/lib/utils/market-hours').then(({ getMarketStatus }) => { + setMarketStatus(getMarketStatus()); + }); + // Refresh every 60s + const interval = setInterval(() => { + import('@/lib/utils/market-hours').then(({ getMarketStatus: gms }) => { + setMarketStatus(gms()); + }); + }, 60000); + return () => clearInterval(interval); + }, [loadConfig]); + + const handleSave = async () => { + setSaving(true); + setMessage(null); + const { saveAIConfig } = await import('@/lib/actions/ai-trading.actions'); + const res = await saveAIConfig(accountId, userId, { + apiEndpoint, + apiKey, + model, + systemPrompt, + strategy, + maxPositionPct, + stopLossPct, + tradingIntervalMin, + }); + setSaving(false); + if (res.success) { + if (res.connectionTest) { + if (res.connectionTest.ok) { + setMessage({ text: res.connectionTest.message, type: 'success' }); + } else { + setMessage({ text: res.connectionTest.message, type: 'error' }); + } + } else { + setMessage({ text: 'AI config saved', type: 'success' }); + } + setConfig(res.config); + } else { + setMessage({ text: res.error || 'Save failed', type: 'error' }); + } + }; + + const handleToggle = async () => { + const newEnabled = !config?.enabled; + const { toggleAITrading } = await import('@/lib/actions/ai-trading.actions'); + const res = await toggleAITrading(accountId, newEnabled); + if (res.success) { + setConfig(prev => prev ? { ...prev, enabled: newEnabled } : null); + setMessage({ text: newEnabled ? 'AI auto-trading enabled' : 'AI auto-trading stopped', type: 'success' }); + } + }; + + const handleStrategyPreset = (s: string) => { + setStrategy(s); + if (s === 'custom') return; // Don't override anything for custom + setSystemPrompt(STRATEGY_PRESET_PROMPTS[s] || ''); + if (s === 'aggressive') { setMaxPositionPct(25); setStopLossPct(-10); } + if (s === 'moderate') { setMaxPositionPct(20); setStopLossPct(-8); } + if (s === 'conservative') { setMaxPositionPct(10); setStopLossPct(-5); } + }; + + const si = STRATEGY_INFO[strategy] || STRATEGY_INFO.moderate; + const StrategyIcon = si.icon; + + return ( +
+ {/* Header */} +
setExpanded(!expanded)} + > +
+
+ +
+
+

+ AI Auto Trading + {config?.enabled && ( + + Running + + )} + {marketStatus && ( + + + {marketStatus.isOpen ? 'Market Open' : 'Market Closed'} + + )} +

+

+ {config?.enabled + ? `Strategy: ${si.label} · Check every ${config.tradingIntervalMin} min` + : 'Configure LLM API for AI auto-trading'} +

+
+
+
+ {expanded ? : } +
+
+ + {/* Expanded Content */} + {expanded && ( +
+ {/* Message */} + {message && ( +
+ {message.text} +
+ )} + + {/* Enable Toggle */} +
+
+

Enable AI Auto Trading

+

+ AI will analyze markets and execute trades on your configured schedule +

+
+ +
+ + {/* Strategy Presets */} +
+ +
+ {Object.entries(STRATEGY_INFO).map(([key, info]) => { + const Icon = info.icon; + return ( + + ); + })} +
+
+ + {strategy === 'custom' && ( +
+ 💡 Custom mode: Write your full trading strategy in the System Prompt below. + Advanced parameters (position sizing, stop-loss, etc.) are fully under your control. +
+ )} + + {/* API Config */} +
+
+ + setApiEndpoint(e.target.value)} + placeholder="https://api.openai.com/v1/chat/completions" + className="w-full bg-black/40 border border-white/10 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-teal-500" + /> +
+
+ + setModel(e.target.value)} + placeholder="gpt-4o / claude-3-opus / deepseek-v3" + className="w-full bg-black/40 border border-white/10 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-teal-500" + /> +
+
+ +
+ setApiKey(e.target.value)} + placeholder="sk-..." + className="w-full bg-black/40 border border-white/10 rounded-lg px-3 py-2 pr-10 text-white text-sm font-mono focus:outline-none focus:border-teal-500" + /> + +
+
+
+ + {/* Advanced Settings */} +
+
+ + setMaxPositionPct(Number(e.target.value))} + min={5} max={100} + className="w-full bg-black/40 border border-white/10 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-teal-500" + /> +
+
+ + setStopLossPct(Number(e.target.value))} + max={0} min={-50} + className="w-full bg-black/40 border border-white/10 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-teal-500" + /> +
+
+ + setTradingIntervalMin(Number(e.target.value))} + min={5} max={1440} + className="w-full bg-black/40 border border-white/10 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-teal-500" + /> +
+
+ + {/* System Prompt */} +
+ +