feat(settings): add settings route and API updates\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,37 +1,9 @@
|
||||
import type { AlpacaAccount } from "../../../types";
|
||||
import Alpaca from "@alpacahq/alpaca-trade-api";
|
||||
import alpacaService from "../../../lib/alpacaClient";
|
||||
|
||||
const alpaca = new Alpaca({
|
||||
keyId: process.env.ALPACA_API_KEY!,
|
||||
secretKey: process.env.ALPACA_SECRET_KEY!,
|
||||
baseUrl: process.env.ALPACA_BASE_URL || "https://paper-api.alpaca.markets",
|
||||
dataBaseUrl: process.env.ALPACA_DATA_URL || "https://data.alpaca.markets",
|
||||
retryOnError: false,
|
||||
});
|
||||
|
||||
async function fetchAlpacaAccount(): Promise<AlpacaAccount> {
|
||||
export async function loader({ request }: { request: Request }) {
|
||||
try {
|
||||
console.log("Fetching Alpaca account with key:", process.env.ALPACA_API_KEY?.substring(0, 8) + "...");
|
||||
const account = await alpaca.getAccount();
|
||||
console.log("Alpaca account fetched successfully");
|
||||
return {
|
||||
cash: parseFloat(account.cash),
|
||||
buying_power: parseFloat(account.buying_power),
|
||||
portfolio_value: parseFloat(account.portfolio_value),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Alpaca API fetch error:", error);
|
||||
return {
|
||||
cash: 0,
|
||||
buying_power: 0,
|
||||
portfolio_value: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function loader() {
|
||||
try {
|
||||
const account = await fetchAlpacaAccount();
|
||||
const account = await alpacaService.fetchAccount();
|
||||
return Response.json(account);
|
||||
} catch (error) {
|
||||
console.error("Alpaca API error:", error);
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
import { fetchBars, fetchLatestBar } from "../../../lib/alpacaClient";
|
||||
import alpacaService from "../../../lib/alpacaClient";
|
||||
|
||||
export async function loader({ request, params }: { request: Request; params: { ticker: string } }) {
|
||||
const ticker = params.ticker?.toUpperCase();
|
||||
const url = new URL(request.url);
|
||||
const timeframe = url.searchParams.get("timeframe") || "1D";
|
||||
const range = url.searchParams.get("range") || "1M"; // 1D, 1W, 1M, 3M, 1Y, 3Y, ALL
|
||||
const mode = url.searchParams.get('mode') === 'live' ? 'live' : 'paper';
|
||||
|
||||
|
||||
if (!ticker) {
|
||||
return Response.json({ error: "Ticker is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
// Normalize timeframe to Alpaca API expected values
|
||||
function mapToAlpacaTimeframe(tf: string) {
|
||||
switch (tf) {
|
||||
case "1H":
|
||||
return "1Hour";
|
||||
case "1D":
|
||||
return "1Day";
|
||||
case "1W":
|
||||
case "1M":
|
||||
return "1Day"; // weekly/monthly UI ranges use daily bars
|
||||
default:
|
||||
return tf; // 1Min,5Min,15Min,30Min expected to be supported
|
||||
}
|
||||
}
|
||||
const alpacaTimeframe = mapToAlpacaTimeframe(timeframe);
|
||||
|
||||
// Get latest bar for current price (uses paper by default unless mode=live)
|
||||
let price = 0;
|
||||
try {
|
||||
const last = await fetchLatestBar(ticker, timeframe, mode as any);
|
||||
const last = await alpacaService.fetchLatestBar(ticker, alpacaTimeframe);
|
||||
price = last ? (last.ClosePrice ?? last.c ?? 0) : 0;
|
||||
} catch (tradeErr) {
|
||||
console.error(`API quote/${ticker}: fetchLatestBar failed`, tradeErr);
|
||||
@@ -44,13 +59,15 @@ export async function loader({ request, params }: { request: Request; params: {
|
||||
}
|
||||
|
||||
const barsOptions: any = { limit: 1000 }; // High limit for time range
|
||||
if (!isIntraday && range !== "ALL") {
|
||||
barsOptions.start = startDate.toISOString().split('T')[0];
|
||||
} else if (!isIntraday) {
|
||||
// For daily/non-intraday queries pass just the date part (YYYY-MM-DD)
|
||||
if (!isIntraday) {
|
||||
barsOptions.start = startDate.toISOString().split('T')[0];
|
||||
} else {
|
||||
// For intraday, pass full ISO start to be precise
|
||||
barsOptions.start = startDate.toISOString();
|
||||
}
|
||||
|
||||
const barsArray = await fetchBars(ticker, timeframe, barsOptions, mode as any);
|
||||
const barsArray = await alpacaService.fetchBars(ticker, alpacaTimeframe, barsOptions);
|
||||
|
||||
// Transform to chart format
|
||||
const transformedBars = barsArray.map((bar: any) => {
|
||||
@@ -59,18 +76,31 @@ export async function loader({ request, params }: { request: Request; params: {
|
||||
const high = typeof bar.HighPrice === 'number' ? bar.HighPrice : (typeof bar.h === 'number' ? bar.h : 0);
|
||||
const low = typeof bar.LowPrice === 'number' ? bar.LowPrice : (typeof bar.l === 'number' ? bar.l : 0);
|
||||
const close = typeof bar.ClosePrice === 'number' ? bar.ClosePrice : (typeof bar.c === 'number' ? bar.c : 0);
|
||||
const timestamp = bar.Timestamp ?? bar.t;
|
||||
const volume = typeof bar.Volume === 'number' ? bar.Volume : (typeof bar.v === 'number' ? bar.v : 0);
|
||||
|
||||
|
||||
// Normalize timestamp to ISO string so client can parse reliably
|
||||
const rawTs = bar.Timestamp ?? bar.t ?? bar.T ?? bar.timestamp;
|
||||
let dateObj: Date | null = null;
|
||||
if (rawTs != null) {
|
||||
if (typeof rawTs === 'number') {
|
||||
// If it's likely in seconds (< 1e12) convert to ms
|
||||
const asMs = rawTs > 1e12 ? rawTs : rawTs * 1000;
|
||||
dateObj = new Date(asMs);
|
||||
} else {
|
||||
dateObj = new Date(rawTs);
|
||||
}
|
||||
}
|
||||
const iso = dateObj && !isNaN(dateObj.getTime()) ? dateObj.toISOString() : null;
|
||||
|
||||
return {
|
||||
t: timestamp,
|
||||
t: iso,
|
||||
o: open,
|
||||
h: high,
|
||||
l: low,
|
||||
c: close,
|
||||
v: volume,
|
||||
};
|
||||
}).filter((bar: any) => bar.o > 0 && bar.h > 0 && bar.l > 0 && bar.c > 0);
|
||||
}).filter((bar: any) => bar.o > 0 && bar.h > 0 && bar.l > 0 && bar.c > 0 && bar.t);
|
||||
|
||||
return Response.json({
|
||||
ticker,
|
||||
|
||||
Reference in New Issue
Block a user