Skip to content

Commit d9aff5b

Browse files
committed
FinCLI Update 1.7.0
1 parent 138929c commit d9aff5b

57 files changed

Lines changed: 634 additions & 358 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.npmignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ build/
2222
dist/
2323
*.egg-info/
2424
CLAUDE.md
25-
UsersMSIAppDataLocalTemp*/
25+
UsersMSIAppDataLocalTemp*/
26+
node_modules/

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# FinCLI v1.6.0
1+
# FinCLI v1.7.0
22

33
[![npm version](https://img.shields.io/npm/v/@drico2008/fincli)](https://www.npmjs.com/package/@drico2008/fincli)
44
[![npm downloads](https://img.shields.io/npm/dm/@drico2008/fincli?label=downloads%2Fmonth)](https://www.npmjs.com/package/@drico2008/fincli)
@@ -260,6 +260,16 @@ fincli
260260

261261
## Changelog
262262

263+
### v1.7.0
264+
- Complete Indonesian → English translation (250+ strings across 20+ files)
265+
- SQLite WAL mode for better concurrent read performance
266+
- API key rotation: `/secrets age` and `/secrets rotate <KEY>` commands
267+
- Key age warnings in `/security status` (alerts when keys > 90 days old)
268+
- Dependency audit: `pip-audit` integrated into prepublish check
269+
- Property-based testing with Hypothesis (crypto, formatting)
270+
- Test infrastructure: `@pytest.mark.integration` and `@pytest.mark.slow` markers
271+
- Add `hypothesis` dev dependency
272+
263273
### v1.6.0
264274
- Ollama local LLM support (offline AI, no API key needed)
265275
- Deprecate dead features: `/provider insider`, `/provider ipo`, `/security encrypt-key`, `/security decrypt-key`, `/trading algo`

fincli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""FinCLI package."""
22

3-
__version__ = "1.6.0"
3+
__version__ = "1.7.0"

fincli/app/analysis/indicators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def phase_one_indicator_status() -> str:
3232
def summarize_technical_indicators(candles: list[Candle]) -> TechnicalSummary:
3333
"""Calculate a compact technical summary from OHLCV candles."""
3434
if not candles:
35-
raise ValueError("Data candle kosong.")
35+
raise ValueError("Candle data is empty.")
3636

3737
closes = [float(candle.close) for candle in candles]
3838
highs = [float(candle.high) for candle in candles]

fincli/app/analysis/market_structure.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def phase_one_structure_status() -> str:
2626
def analyze_market_structure(candles: list[Candle], lookback: int = 20) -> MarketStructureSummary:
2727
"""Detect a compact HH/HL/LH/LL-style market structure summary."""
2828
if not candles:
29-
raise ValueError("Data candle kosong.")
29+
raise ValueError("Candle data is empty.")
3030

3131
recent = candles[-lookback:]
3232
highs = [float(candle.high) for candle in recent]

fincli/app/analysis/trading_methods.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class TradingMethodContext:
2525

2626
def analyze_trading_methods(candles: list[Candle], left: int = 3, right: int = 3) -> TradingMethodContext:
2727
if not candles:
28-
raise ValueError("Data candle kosong.")
28+
raise ValueError("Candle data is empty.")
2929

3030
pivot_highs, pivot_lows = _pivots(candles, left, right)
3131
recent = candles[-20:]

fincli/app/brokers/alpaca.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ async def connect(self, mode: str = "paper") -> BrokerConnectionStatus:
124124
broker=self.name,
125125
mode=mode,
126126
account_id=None,
127-
message=f"Mode tidak didukung: {mode}. Gunakan 'paper' atau 'live'.",
127+
message=f"Unsupported mode: {mode}. Use 'paper' or 'live'.",
128128
)
129129

130130
if not self.api_key or not self.secret_key:
@@ -133,7 +133,7 @@ async def connect(self, mode: str = "paper") -> BrokerConnectionStatus:
133133
broker=self.name,
134134
mode=mode,
135135
account_id=None,
136-
message="API key atau secret key belum diatur. Set ALPACA_API_KEY dan ALPACA_SECRET_KEY.",
136+
message="API key or secret key not set. Set ALPACA_API_KEY and ALPACA_SECRET_KEY.",
137137
)
138138

139139
# Set correct base URL
@@ -166,7 +166,7 @@ async def connect(self, mode: str = "paper") -> BrokerConnectionStatus:
166166
broker=self.name,
167167
mode=mode,
168168
account_id=None,
169-
message=f"Gagal terhubung ke Alpaca: {exc}",
169+
message=f"Failed to connect to Alpaca: {exc}",
170170
)
171171

172172
async def disconnect(self) -> None:
@@ -197,9 +197,9 @@ async def get_account(self) -> BrokerAccount:
197197
broker=self.name,
198198
)
199199
except httpx.HTTPStatusError as exc:
200-
raise ProviderError(f"Gagal mengambil account info: HTTP {exc.response.status_code}") from exc
200+
raise ProviderError(f"Failed to get account info: HTTP {exc.response.status_code}") from exc
201201
except Exception as exc:
202-
raise ProviderError(f"Gagal mengambil account info: {exc}") from exc
202+
raise ProviderError(f"Failed to get account info: {exc}") from exc
203203

204204
async def get_positions(self) -> list[BrokerPosition]:
205205
"""Get all open positions."""
@@ -220,9 +220,9 @@ async def get_positions(self) -> list[BrokerPosition]:
220220
))
221221
return positions
222222
except httpx.HTTPStatusError as exc:
223-
raise ProviderError(f"Gagal mengambil posisi: HTTP {exc.response.status_code}") from exc
223+
raise ProviderError(f"Failed to get positions: HTTP {exc.response.status_code}") from exc
224224
except Exception as exc:
225-
raise ProviderError(f"Gagal mengambil posisi: {exc}") from exc
225+
raise ProviderError(f"Failed to get positions: {exc}") from exc
226226

227227
async def place_order(
228228
self,
@@ -270,9 +270,9 @@ async def place_order(
270270
except (json.JSONDecodeError, ValueError):
271271
pass
272272
msg = error_data.get("message", f"HTTP {exc.response.status_code}")
273-
raise ProviderError(f"Gagal place order: {msg}") from exc
273+
raise ProviderError(f"Failed to place order: {msg}") from exc
274274
except Exception as exc:
275-
raise ProviderError(f"Gagal place order: {exc}") from exc
275+
raise ProviderError(f"Failed to place order: {exc}") from exc
276276

277277
async def cancel_order(self, broker_order_id: str) -> BrokerOrder:
278278
"""Cancel a pending order."""
@@ -282,9 +282,9 @@ async def cancel_order(self, broker_order_id: str) -> BrokerOrder:
282282
response.raise_for_status()
283283
return _parse_order(response.json(), self.name)
284284
except httpx.HTTPStatusError as exc:
285-
raise ProviderError(f"Gagal cancel order: HTTP {exc.response.status_code}") from exc
285+
raise ProviderError(f"Failed to cancel order: HTTP {exc.response.status_code}") from exc
286286
except Exception as exc:
287-
raise ProviderError(f"Gagal cancel order: {exc}") from exc
287+
raise ProviderError(f"Failed to cancel order: {exc}") from exc
288288

289289
async def get_order(self, broker_order_id: str) -> BrokerOrder:
290290
"""Get order status."""
@@ -294,9 +294,9 @@ async def get_order(self, broker_order_id: str) -> BrokerOrder:
294294
response.raise_for_status()
295295
return _parse_order(response.json(), self.name)
296296
except httpx.HTTPStatusError as exc:
297-
raise ProviderError(f"Gagal mengambil order: HTTP {exc.response.status_code}") from exc
297+
raise ProviderError(f"Failed to get order: HTTP {exc.response.status_code}") from exc
298298
except Exception as exc:
299-
raise ProviderError(f"Gagal mengambil order: {exc}") from exc
299+
raise ProviderError(f"Failed to get order: {exc}") from exc
300300

301301
async def list_orders(self, status: str | None = None, limit: int = 50) -> list[BrokerOrder]:
302302
"""List orders, optionally filtered by status."""
@@ -318,9 +318,9 @@ async def list_orders(self, status: str | None = None, limit: int = 50) -> list[
318318
response.raise_for_status()
319319
return [_parse_order(order, self.name) for order in response.json()]
320320
except httpx.HTTPStatusError as exc:
321-
raise ProviderError(f"Gagal mengambil orders: HTTP {exc.response.status_code}") from exc
321+
raise ProviderError(f"Failed to list orders: HTTP {exc.response.status_code}") from exc
322322
except Exception as exc:
323-
raise ProviderError(f"Gagal mengambil orders: {exc}") from exc
323+
raise ProviderError(f"Failed to list orders: {exc}") from exc
324324

325325
async def get_quote(self, symbol: str) -> float:
326326
"""Get current price for a symbol."""
@@ -332,6 +332,6 @@ async def get_quote(self, symbol: str) -> float:
332332
quote = data.get("quote", {})
333333
return float(quote.get("ap", 0) or quote.get("bp", 0) or 0)
334334
except httpx.HTTPStatusError as exc:
335-
raise ProviderError(f"Gagal mengambil quote: HTTP {exc.response.status_code}") from exc
335+
raise ProviderError(f"Failed to get quote: HTTP {exc.response.status_code}") from exc
336336
except Exception as exc:
337-
raise ProviderError(f"Gagal mengambil quote: {exc}") from exc
337+
raise ProviderError(f"Failed to get quote: {exc}") from exc

fincli/app/brokers/registry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def create(self, name: str) -> BaseBroker:
6767
if name_lower == "binance":
6868
from fincli.app.brokers.binance import BinanceBroker
6969
return BinanceBroker()
70-
raise ValueError(f"Broker tidak didukung: {name}. Broker tersedia: {', '.join(BROKER_CATALOG.keys())}")
70+
raise ValueError(f"Unsupported broker: {name}. Available brokers: {', '.join(BROKER_CATALOG.keys())}")
7171

7272
async def connect(self, name: str, mode: str = "paper") -> BrokerConnectionStatus:
7373
"""Connect to a broker."""

0 commit comments

Comments
 (0)