@drico2008/fincli 0.1.3 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +909 -684
- package/fincli/__init__.py +3 -3
- package/fincli/app/agents/__init__.py +5 -0
- package/fincli/app/agents/registry.py +76 -0
- package/fincli/app/analysis/ai_prompts.py +23 -16
- package/fincli/app/analysis/analyzer.py +107 -100
- package/fincli/app/analysis/assistant_context.py +187 -160
- package/fincli/app/analysis/backtest.py +179 -0
- package/fincli/app/analysis/gameplay_plan.py +79 -0
- package/fincli/app/analysis/indicators.py +1 -1
- package/fincli/app/analysis/market_structure.py +1 -1
- package/fincli/app/analysis/multi_timeframe.py +180 -0
- package/fincli/app/analysis/trading_methods.py +144 -0
- package/fincli/app/cli/commands.py +105 -77
- package/fincli/app/cli/router.py +2143 -1121
- package/fincli/app/connectors/__init__.py +5 -0
- package/fincli/app/connectors/catalog.py +148 -0
- package/fincli/app/connectors/news_connectors.py +412 -0
- package/fincli/app/modules/alerts.py +80 -0
- package/fincli/app/modules/economic_calendar.py +374 -1
- package/fincli/app/modules/reports.py +151 -0
- package/fincli/app/modules/scanner.py +111 -93
- package/fincli/app/modules/session_history.py +113 -0
- package/fincli/app/modules/transactions.py +84 -84
- package/fincli/app/modules/user_profile.py +84 -0
- package/fincli/app/plugins/loader.py +72 -0
- package/fincli/app/providers/ai/anthropic_provider.py +8 -7
- package/fincli/app/providers/ai/gemini_provider.py +8 -7
- package/fincli/app/providers/ai/groq_provider.py +8 -7
- package/fincli/app/providers/ai/huggingface_provider.py +8 -7
- package/fincli/app/providers/ai/manager.py +60 -60
- package/fincli/app/providers/ai/openai_provider.py +8 -7
- package/fincli/app/providers/ai/openrouter_provider.py +8 -7
- package/fincli/app/providers/ai/together_provider.py +8 -7
- package/fincli/app/providers/market/alphavantage_provider.py +194 -0
- package/fincli/app/providers/market/base.py +98 -77
- package/fincli/app/providers/market/custom_provider.py +186 -169
- package/fincli/app/providers/market/manager.py +85 -2
- package/fincli/app/providers/market/news_provider.py +4 -4
- package/fincli/app/providers/market/symbols.py +143 -0
- package/fincli/app/providers/market/twelvedata_provider.py +167 -167
- package/fincli/app/providers/market/yfinance_provider.py +1 -1
- package/fincli/app/research/__init__.py +7 -0
- package/fincli/app/research/engine.py +75 -0
- package/fincli/app/research/formatter.py +22 -0
- package/fincli/app/research/models.py +18 -0
- package/fincli/app/research/prompt_builder.py +47 -0
- package/fincli/app/services/macro_data.py +50 -0
- package/fincli/app/services/market_data.py +203 -203
- package/fincli/app/services/news_aggregator.py +90 -0
- package/fincli/app/services/web_research.py +267 -0
- package/fincli/app/storage/cache.py +2 -2
- package/fincli/app/storage/config.py +122 -88
- package/fincli/app/storage/database.py +201 -85
- package/fincli/app/storage/secrets.py +12 -3
- package/fincli/app/tui/components.py +68 -50
- package/fincli/app/tui/layout.py +270 -258
- package/fincli/app/tui/market_provider_selector.py +6 -1
- package/fincli/app/tui/model_selector.py +11 -3
- package/fincli/app/tui/theme.py +134 -74
- package/fincli/app/utils/formatting.py +125 -12
- package/npm/bin/fincli.js +9 -2
- package/package.json +23 -23
- package/pyproject.toml +35 -35
|
@@ -1,161 +1,188 @@
|
|
|
1
|
-
"""FinCLI AI assistant prompt and safety helpers."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import re
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
FINCLI_ASSISTANT_SYSTEM_PROMPT = """
|
|
9
|
-
You are FinCLI AI Assistance, the embedded assistant inside FinCLI v0.
|
|
10
|
-
|
|
11
|
-
Identity and scope:
|
|
1
|
+
"""FinCLI AI assistant prompt and safety helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
FINCLI_ASSISTANT_SYSTEM_PROMPT = """
|
|
9
|
+
You are FinCLI AI Assistance, the embedded assistant inside FinCLI v0.2.2.
|
|
10
|
+
|
|
11
|
+
Identity and scope:
|
|
12
12
|
- FinCLI is a terminal-first financial dashboard for market data, news/fundamentals, technical analysis, watchlists, portfolios, journals, and provider configuration.
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
r"\b(
|
|
41
|
-
r"\b(
|
|
42
|
-
r"\b(
|
|
43
|
-
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
"
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
"
|
|
122
|
-
"atau
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if
|
|
135
|
-
continue
|
|
136
|
-
if
|
|
137
|
-
continue
|
|
138
|
-
if symbol
|
|
139
|
-
continue
|
|
140
|
-
if symbol
|
|
141
|
-
|
|
142
|
-
if
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
f"{
|
|
154
|
-
"
|
|
155
|
-
f"{
|
|
156
|
-
"
|
|
157
|
-
"
|
|
158
|
-
"
|
|
159
|
-
"-
|
|
160
|
-
"-
|
|
161
|
-
|
|
13
|
+
- FinCLI commands start with slash commands inside the TUI, for example /help, /research AAPL --quick, /macro US, /profile, /technical AAPL, and /analyze XAUUSD.
|
|
14
|
+
- Your role is to help users understand markets, risk, portfolio context, trading journal patterns, FinCLI commands, and general non-coding questions.
|
|
15
|
+
- Free chat is allowed, but you must keep your identity as FinCLI's assistant and be clear when market data is unavailable or delayed.
|
|
16
|
+
|
|
17
|
+
Financial analysis rules:
|
|
18
|
+
- Analyze from provided market context first. Do not invent prices, news, fundamentals, provider status, or certainty.
|
|
19
|
+
- If Web Research Context is provided, use it as current public context, mention source URLs, and separate sourced facts from interpretation.
|
|
20
|
+
- Use probabilistic language: scenario, bias, confirmation, invalidation, risk, caution.
|
|
21
|
+
- Do not promise profit and do not present aggressive entries as guaranteed signals.
|
|
22
|
+
- For technical analysis, weigh trend, momentum, volatility, support/resistance, market structure, and data quality.
|
|
23
|
+
- For fundamental analysis, separate valuation, growth, profitability, sector context, balance-sheet risk, and missing-data limitations.
|
|
24
|
+
- Always include a short non-financial-advice reminder when discussing instruments, portfolio, or trading decisions.
|
|
25
|
+
|
|
26
|
+
Coding boundary:
|
|
27
|
+
- Do not provide coding, debugging, refactoring, implementation plans, source code, scripts, commands for software builds, or programming architecture.
|
|
28
|
+
- If asked for coding help, refuse briefly and redirect to FinCLI usage, market analysis, provider setup, risk management, or journal/portfolio workflow.
|
|
29
|
+
|
|
30
|
+
Response style:
|
|
31
|
+
- Be concise, structured, and practical.
|
|
32
|
+
- Prefer bullets for market analysis.
|
|
33
|
+
- If the user asks casual non-market questions, answer normally while staying within the coding boundary.
|
|
34
|
+
""".strip()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_CODING_PATTERNS = tuple(
|
|
38
|
+
re.compile(pattern, re.IGNORECASE)
|
|
39
|
+
for pattern in (
|
|
40
|
+
r"\b(write|generate|buat|bikin|create|implement|refactor|debug|fix|compile|build)\s+(code|kode|script|program|function|fungsi|class|module|modul|app|website|backend|frontend)\b",
|
|
41
|
+
r"\b(code|kode|source code|script|programming|pemrograman|debugging|traceback|stack trace)\b",
|
|
42
|
+
r"\b(python|javascript|typescript|react|next\.?js|node\.?js|npm|pip|django|flask|fastapi|sql|docker|kubernetes|regex)\b",
|
|
43
|
+
r"\b(git|commit|pull request|unit test|pytest|lint|ci/cd|api endpoint|sdk|library|framework)\b",
|
|
44
|
+
r"\b(error di kode|bug di kode|perbaiki kode|arsitektur software|software architecture)\b",
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_SYMBOL_PATTERN = re.compile(r"\b[A-Z][A-Z0-9./:_-]{1,14}\b", re.IGNORECASE)
|
|
50
|
+
_MARKET_KEYWORDS = {
|
|
51
|
+
"AAPL",
|
|
52
|
+
"MSFT",
|
|
53
|
+
"NVDA",
|
|
54
|
+
"TSLA",
|
|
55
|
+
"AMZN",
|
|
56
|
+
"GOOGL",
|
|
57
|
+
"META",
|
|
58
|
+
"BTC",
|
|
59
|
+
"ETH",
|
|
60
|
+
"BTCUSD",
|
|
61
|
+
"BTCUSDT",
|
|
62
|
+
"ETHUSD",
|
|
63
|
+
"ETHUSDT",
|
|
64
|
+
"EURUSD",
|
|
65
|
+
"GBPUSD",
|
|
66
|
+
"USDJPY",
|
|
67
|
+
"AUDUSD",
|
|
68
|
+
"USDCAD",
|
|
69
|
+
"USDCHF",
|
|
70
|
+
"XAUUSD",
|
|
71
|
+
"XAGUSD",
|
|
72
|
+
"GOLD",
|
|
73
|
+
"SILVER",
|
|
74
|
+
"WTI",
|
|
75
|
+
"BRENT",
|
|
76
|
+
"SPX",
|
|
77
|
+
"SP500",
|
|
78
|
+
"NASDAQ",
|
|
79
|
+
"NDX",
|
|
80
|
+
"DOW",
|
|
81
|
+
"DAX",
|
|
82
|
+
"FTSE",
|
|
83
|
+
"NIKKEI",
|
|
84
|
+
"HSI",
|
|
85
|
+
}
|
|
86
|
+
_SYMBOL_STOPWORDS = {
|
|
87
|
+
"AI",
|
|
88
|
+
"API",
|
|
89
|
+
"CLI",
|
|
90
|
+
"TUI",
|
|
91
|
+
"FINCLI",
|
|
92
|
+
"BUY",
|
|
93
|
+
"SELL",
|
|
94
|
+
"CAUTION",
|
|
95
|
+
"RSI",
|
|
96
|
+
"MACD",
|
|
97
|
+
"EMA",
|
|
98
|
+
"SMA",
|
|
99
|
+
"ATR",
|
|
100
|
+
"PE",
|
|
101
|
+
"EPS",
|
|
102
|
+
"USD",
|
|
103
|
+
"EUR",
|
|
104
|
+
"GBP",
|
|
105
|
+
"JPY",
|
|
106
|
+
"IDR",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def is_coding_request(prompt: str) -> bool:
|
|
111
|
+
"""Return True when a free-chat prompt asks for programming help."""
|
|
112
|
+
normalized = prompt.strip()
|
|
113
|
+
if not normalized:
|
|
114
|
+
return False
|
|
115
|
+
return any(pattern.search(normalized) for pattern in _CODING_PATTERNS)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def coding_refusal() -> str:
|
|
119
|
+
"""Return FinCLI-specific refusal text for coding prompts."""
|
|
120
|
+
return (
|
|
121
|
+
"Aku FinCLI AI Assistance untuk market, portfolio, journal, provider, dan risk workflow. "
|
|
122
|
+
"Aku tidak menangani coding, debugging, refactor, atau pembuatan software di dalam FinCLI. "
|
|
123
|
+
"Kamu bisa tanya analisis market, fundamental, technical setup, watchlist, portfolio, journal, "
|
|
124
|
+
"atau cara memakai command FinCLI."
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def extract_market_symbols(prompt: str, limit: int = 3) -> list[str]:
|
|
129
|
+
"""Extract explicit market symbols from a user prompt."""
|
|
130
|
+
found: list[str] = []
|
|
131
|
+
for match in _SYMBOL_PATTERN.finditer(prompt):
|
|
132
|
+
raw_symbol = match.group(0).strip(".,?!:;()[]{}")
|
|
133
|
+
symbol = raw_symbol.upper()
|
|
134
|
+
if symbol in _SYMBOL_STOPWORDS:
|
|
135
|
+
continue
|
|
136
|
+
if not raw_symbol.isupper() and symbol not in _MARKET_KEYWORDS:
|
|
137
|
+
continue
|
|
138
|
+
if len(symbol) <= 2 and symbol not in _MARKET_KEYWORDS:
|
|
139
|
+
continue
|
|
140
|
+
if symbol.isdigit():
|
|
141
|
+
continue
|
|
142
|
+
if symbol not in found:
|
|
143
|
+
found.append(symbol)
|
|
144
|
+
if len(found) >= limit:
|
|
145
|
+
break
|
|
146
|
+
return found
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def build_fincli_assistant_prompt(user_prompt: str, market_context: str = "") -> str:
|
|
150
|
+
"""Build the final prompt sent to the configured AI provider."""
|
|
151
|
+
context = market_context.strip() or "No explicit market context was available for this free-chat prompt."
|
|
152
|
+
return (
|
|
153
|
+
f"{FINCLI_ASSISTANT_SYSTEM_PROMPT}\n\n"
|
|
154
|
+
"Runtime Market Context:\n"
|
|
155
|
+
f"{context}\n\n"
|
|
156
|
+
"User Prompt:\n"
|
|
157
|
+
f"{user_prompt.strip()}\n\n"
|
|
158
|
+
"Instruction:\n"
|
|
159
|
+
"- Answer the user's prompt directly.\n"
|
|
160
|
+
"- If market or web context is present, cite provider/data-quality limitations and source URLs when available.\n"
|
|
161
|
+
"- If market context is missing and the user asks about an instrument, say what data is missing.\n"
|
|
162
|
+
"- Keep the coding boundary enforced.\n"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def build_web_research_answer_prompt(user_prompt: str, web_context: str) -> str:
|
|
167
|
+
"""Build a prompt that turns gathered web context into an answer, not a source dump."""
|
|
168
|
+
context = web_context.strip() or "Web Research: no public web context returned."
|
|
169
|
+
return (
|
|
170
|
+
f"{FINCLI_ASSISTANT_SYSTEM_PROMPT}\n\n"
|
|
171
|
+
"Web Search Skill Result:\n"
|
|
172
|
+
f"{context}\n\n"
|
|
173
|
+
"User Prompt:\n"
|
|
174
|
+
f"{user_prompt.strip()}\n\n"
|
|
175
|
+
"Instruction:\n"
|
|
176
|
+
"- You already have web search context above. Do not answer by only listing articles or links.\n"
|
|
177
|
+
"- Synthesize the sources into a useful explanation/summary for the user.\n"
|
|
178
|
+
"- Prioritize facts found in the web context, then clearly label interpretation.\n"
|
|
179
|
+
"- If sources disagree or are thin, say that the evidence is limited.\n"
|
|
180
|
+
"- Use this output structure when relevant:\n"
|
|
181
|
+
" 1. Ringkasan singkat\n"
|
|
182
|
+
" 2. Poin utama/penyebab\n"
|
|
183
|
+
" 3. Dampak atau implikasi\n"
|
|
184
|
+
" 4. Risiko dan hal yang perlu diverifikasi\n"
|
|
185
|
+
" 5. Sumber singkat\n"
|
|
186
|
+
"- Keep source citations compact: source title or URL only where useful.\n"
|
|
187
|
+
"- Do not provide financial advice or certainty about market direction.\n"
|
|
188
|
+
)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Lightweight rule-based backtesting for FinCLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from fincli.app.providers.market.base import Candle
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class BacktestTrade:
|
|
12
|
+
entry_index: int
|
|
13
|
+
exit_index: int
|
|
14
|
+
entry_price: float
|
|
15
|
+
exit_price: float
|
|
16
|
+
pnl_percent: float
|
|
17
|
+
reason: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class BacktestResult:
|
|
22
|
+
symbol: str
|
|
23
|
+
strategy: str
|
|
24
|
+
interval: str
|
|
25
|
+
candles: int
|
|
26
|
+
trades: list[BacktestTrade]
|
|
27
|
+
total_return_percent: float
|
|
28
|
+
win_rate: float
|
|
29
|
+
max_drawdown_percent: float
|
|
30
|
+
exposure_percent: float
|
|
31
|
+
notes: tuple[str, ...]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def run_backtest(
|
|
35
|
+
symbol: str,
|
|
36
|
+
candles: list[Candle],
|
|
37
|
+
strategy: str = "sma_cross",
|
|
38
|
+
interval: str = "1d",
|
|
39
|
+
) -> BacktestResult:
|
|
40
|
+
if len(candles) < 30:
|
|
41
|
+
raise ValueError("Backtest needs at least 30 candles.")
|
|
42
|
+
|
|
43
|
+
normalized = strategy.lower().strip()
|
|
44
|
+
if normalized in {"sma", "sma_cross", "ma_cross"}:
|
|
45
|
+
trades = _sma_cross_trades(candles)
|
|
46
|
+
elif normalized in {"rsi", "rsi_reversion", "mean_reversion"}:
|
|
47
|
+
trades = _rsi_reversion_trades(candles)
|
|
48
|
+
else:
|
|
49
|
+
raise ValueError("Unknown strategy. Use sma_cross or rsi_reversion.")
|
|
50
|
+
|
|
51
|
+
equity_curve = _equity_curve(candles, trades)
|
|
52
|
+
total_return = (equity_curve[-1] - 1.0) * 100
|
|
53
|
+
max_drawdown = _max_drawdown(equity_curve)
|
|
54
|
+
wins = sum(1 for trade in trades if trade.pnl_percent > 0)
|
|
55
|
+
win_rate = (wins / len(trades) * 100) if trades else 0.0
|
|
56
|
+
exposure = _exposure(candles, trades)
|
|
57
|
+
notes = _result_notes(trades, total_return, max_drawdown)
|
|
58
|
+
return BacktestResult(
|
|
59
|
+
symbol=symbol.upper(),
|
|
60
|
+
strategy=normalized,
|
|
61
|
+
interval=interval,
|
|
62
|
+
candles=len(candles),
|
|
63
|
+
trades=trades,
|
|
64
|
+
total_return_percent=total_return,
|
|
65
|
+
win_rate=win_rate,
|
|
66
|
+
max_drawdown_percent=max_drawdown,
|
|
67
|
+
exposure_percent=exposure,
|
|
68
|
+
notes=notes,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _sma_cross_trades(candles: list[Candle], fast: int = 10, slow: int = 30) -> list[BacktestTrade]:
|
|
73
|
+
closes = [float(candle.close) for candle in candles]
|
|
74
|
+
position_entry: tuple[int, float] | None = None
|
|
75
|
+
trades: list[BacktestTrade] = []
|
|
76
|
+
for index in range(slow, len(closes)):
|
|
77
|
+
fast_ma = _sma(closes[: index + 1], fast)
|
|
78
|
+
slow_ma = _sma(closes[: index + 1], slow)
|
|
79
|
+
previous_fast = _sma(closes[:index], fast)
|
|
80
|
+
previous_slow = _sma(closes[:index], slow)
|
|
81
|
+
if None in {fast_ma, slow_ma, previous_fast, previous_slow}:
|
|
82
|
+
continue
|
|
83
|
+
bullish_cross = previous_fast <= previous_slow and fast_ma > slow_ma
|
|
84
|
+
bearish_cross = previous_fast >= previous_slow and fast_ma < slow_ma
|
|
85
|
+
if position_entry is None and bullish_cross:
|
|
86
|
+
position_entry = (index, closes[index])
|
|
87
|
+
elif position_entry is not None and bearish_cross:
|
|
88
|
+
entry_index, entry_price = position_entry
|
|
89
|
+
trades.append(_trade(entry_index, index, entry_price, closes[index], "sma bearish cross"))
|
|
90
|
+
position_entry = None
|
|
91
|
+
if position_entry is not None:
|
|
92
|
+
entry_index, entry_price = position_entry
|
|
93
|
+
trades.append(_trade(entry_index, len(closes) - 1, entry_price, closes[-1], "end of test"))
|
|
94
|
+
return trades
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _rsi_reversion_trades(candles: list[Candle], buy_level: float = 30, sell_level: float = 55) -> list[BacktestTrade]:
|
|
98
|
+
closes = [float(candle.close) for candle in candles]
|
|
99
|
+
position_entry: tuple[int, float] | None = None
|
|
100
|
+
trades: list[BacktestTrade] = []
|
|
101
|
+
for index in range(15, len(closes)):
|
|
102
|
+
rsi = _rsi(closes[: index + 1], 14)
|
|
103
|
+
if rsi is None:
|
|
104
|
+
continue
|
|
105
|
+
if position_entry is None and rsi < buy_level:
|
|
106
|
+
position_entry = (index, closes[index])
|
|
107
|
+
elif position_entry is not None and rsi > sell_level:
|
|
108
|
+
entry_index, entry_price = position_entry
|
|
109
|
+
trades.append(_trade(entry_index, index, entry_price, closes[index], "rsi mean reversion exit"))
|
|
110
|
+
position_entry = None
|
|
111
|
+
if position_entry is not None:
|
|
112
|
+
entry_index, entry_price = position_entry
|
|
113
|
+
trades.append(_trade(entry_index, len(closes) - 1, entry_price, closes[-1], "end of test"))
|
|
114
|
+
return trades
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _trade(entry_index: int, exit_index: int, entry_price: float, exit_price: float, reason: str) -> BacktestTrade:
|
|
118
|
+
pnl = ((exit_price / entry_price) - 1.0) * 100
|
|
119
|
+
return BacktestTrade(entry_index, exit_index, entry_price, exit_price, pnl, reason)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _equity_curve(candles: list[Candle], trades: list[BacktestTrade]) -> list[float]:
|
|
123
|
+
equity = 1.0
|
|
124
|
+
curve = [equity]
|
|
125
|
+
trade_by_exit = {trade.exit_index: trade for trade in trades}
|
|
126
|
+
for index in range(1, len(candles)):
|
|
127
|
+
if index in trade_by_exit:
|
|
128
|
+
equity *= 1 + (trade_by_exit[index].pnl_percent / 100)
|
|
129
|
+
curve.append(equity)
|
|
130
|
+
return curve
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _max_drawdown(equity_curve: list[float]) -> float:
|
|
134
|
+
peak = equity_curve[0]
|
|
135
|
+
max_drawdown = 0.0
|
|
136
|
+
for value in equity_curve:
|
|
137
|
+
peak = max(peak, value)
|
|
138
|
+
drawdown = ((value / peak) - 1.0) * 100
|
|
139
|
+
max_drawdown = min(max_drawdown, drawdown)
|
|
140
|
+
return abs(max_drawdown)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _exposure(candles: list[Candle], trades: list[BacktestTrade]) -> float:
|
|
144
|
+
if not candles:
|
|
145
|
+
return 0.0
|
|
146
|
+
bars = sum(max(0, trade.exit_index - trade.entry_index) for trade in trades)
|
|
147
|
+
return min(100.0, (bars / len(candles)) * 100)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _result_notes(trades: list[BacktestTrade], total_return: float, max_drawdown: float) -> tuple[str, ...]:
|
|
151
|
+
notes = ["Backtest is educational and ignores fees, slippage, spreads, liquidity, and survivorship bias."]
|
|
152
|
+
if not trades:
|
|
153
|
+
notes.append("No trades were generated by the selected strategy.")
|
|
154
|
+
if max_drawdown > abs(total_return) and trades:
|
|
155
|
+
notes.append("Drawdown is large relative to return; treat the result as fragile.")
|
|
156
|
+
return tuple(notes)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _sma(values: list[float], window: int) -> float | None:
|
|
160
|
+
if len(values) < window:
|
|
161
|
+
return None
|
|
162
|
+
return sum(values[-window:]) / window
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _rsi(values: list[float], window: int) -> float | None:
|
|
166
|
+
if len(values) <= window:
|
|
167
|
+
return None
|
|
168
|
+
gains: list[float] = []
|
|
169
|
+
losses: list[float] = []
|
|
170
|
+
for previous, current in zip(values[-window - 1 : -1], values[-window:]):
|
|
171
|
+
delta = current - previous
|
|
172
|
+
gains.append(max(delta, 0.0))
|
|
173
|
+
losses.append(abs(min(delta, 0.0)))
|
|
174
|
+
average_gain = sum(gains) / window
|
|
175
|
+
average_loss = sum(losses) / window
|
|
176
|
+
if average_loss == 0:
|
|
177
|
+
return 100.0
|
|
178
|
+
rs = average_gain / average_loss
|
|
179
|
+
return 100 - (100 / (1 + rs))
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Translate local user profile into risk constraints for AI analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from fincli.app.modules.user_profile import UserProfile
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class GameplayPlan:
|
|
12
|
+
gameplay: str
|
|
13
|
+
asset_class: str
|
|
14
|
+
max_sl: str
|
|
15
|
+
minimum_rr: str
|
|
16
|
+
note: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def build_gameplay_plan(profile: UserProfile | None, symbol: str) -> GameplayPlan | None:
|
|
20
|
+
if profile is None:
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
asset_class = infer_asset_class(symbol)
|
|
24
|
+
gameplay = profile.gameplay
|
|
25
|
+
max_sl = _max_sl(gameplay, asset_class, symbol)
|
|
26
|
+
note = (
|
|
27
|
+
f"Profile equity {profile.equity:g} {profile.currency}, leverage {profile.leverage}, "
|
|
28
|
+
f"experience {profile.years_in_investment:g} years. Use risk-aware scenarios, not guarantees."
|
|
29
|
+
)
|
|
30
|
+
return GameplayPlan(gameplay=gameplay, asset_class=asset_class, max_sl=max_sl, minimum_rr="1:1.5", note=note)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def format_gameplay_context(profile: UserProfile | None, symbol: str) -> str:
|
|
34
|
+
plan = build_gameplay_plan(profile, symbol)
|
|
35
|
+
if profile is None or plan is None:
|
|
36
|
+
return (
|
|
37
|
+
"User Gameplay Profile: not configured.\n"
|
|
38
|
+
"Ask user to run /profile set <name> <equity> <currency> <leverage> <years> for tailored SL/TP context."
|
|
39
|
+
)
|
|
40
|
+
return (
|
|
41
|
+
"User Gameplay Profile:\n"
|
|
42
|
+
f"- Name: {profile.name}\n"
|
|
43
|
+
f"- Equity: {profile.equity:g} {profile.currency}\n"
|
|
44
|
+
f"- Leverage: {profile.leverage}\n"
|
|
45
|
+
f"- Experience: {profile.years_in_investment:g} years\n"
|
|
46
|
+
f"- Gameplay: {plan.gameplay}\n"
|
|
47
|
+
f"- Asset Class: {plan.asset_class}\n"
|
|
48
|
+
f"- Max SL Guide: {plan.max_sl}\n"
|
|
49
|
+
f"- Minimum RR: {plan.minimum_rr}\n"
|
|
50
|
+
f"- Risk Note: {plan.note}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def infer_asset_class(symbol: str) -> str:
|
|
55
|
+
normalized = symbol.upper()
|
|
56
|
+
if any(token in normalized for token in ("XAU", "GOLD", "XAG", "SILVER", "WTI", "BRENT", "OIL")):
|
|
57
|
+
return "commodities"
|
|
58
|
+
if any(token in normalized for token in ("SPX", "SP500", "NAS", "NDX", "DAX", "FTSE", "NIKKEI", "HSI", "DJI", "US30")):
|
|
59
|
+
return "indices"
|
|
60
|
+
compact = normalized.replace("/", "").replace("-", "").replace("=", "")
|
|
61
|
+
if len(compact) == 6 and compact[:3].isalpha() and compact[3:].isalpha():
|
|
62
|
+
return "forex"
|
|
63
|
+
return "stocks/crypto"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _max_sl(gameplay: str, asset_class: str, symbol: str) -> str:
|
|
67
|
+
if asset_class == "forex":
|
|
68
|
+
return {"Scalper": "< 15 pips", "Intra day": "< 20 pips", "Day trade": "< 25 pips"}.get(gameplay, "profile-dependent")
|
|
69
|
+
if asset_class == "commodities":
|
|
70
|
+
if gameplay == "Scalper":
|
|
71
|
+
return "< 31 pips"
|
|
72
|
+
if gameplay == "Intra day":
|
|
73
|
+
return "< 60 pips"
|
|
74
|
+
if gameplay == "Day trade":
|
|
75
|
+
return "< 120 pips for gold, < 60 pips for other commodities"
|
|
76
|
+
return "profile-dependent"
|
|
77
|
+
if asset_class == "indices":
|
|
78
|
+
return {"Scalper": "< 51 pips", "Intra day": "< 71 pips", "Day trade": "< 120 pips"}.get(gameplay, "profile-dependent")
|
|
79
|
+
return "use ATR/support-resistance based invalidation; keep RR >= 1:1.5"
|
|
@@ -26,7 +26,7 @@ class TechnicalSummary:
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def phase_one_indicator_status() -> str:
|
|
29
|
-
return "Indicator engine
|
|
29
|
+
return "Indicator engine active: SMA, EMA, RSI, MACD, Bollinger Bands, ATR, volume, support, and resistance."
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def summarize_technical_indicators(candles: list[Candle]) -> TechnicalSummary:
|
|
@@ -20,7 +20,7 @@ class MarketStructureSummary:
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def phase_one_structure_status() -> str:
|
|
23
|
-
return "Market structure
|
|
23
|
+
return "Market structure engine active: HH/HL/LH/LL, BOS, CHoCH, liquidity area, and risk zone."
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def analyze_market_structure(candles: list[Candle], lookback: int = 20) -> MarketStructureSummary:
|