Skip to content

Commit

Permalink
sf buy start time now
Browse files Browse the repository at this point in the history
  • Loading branch information
Sladuca committed Oct 3, 2024
1 parent c888402 commit 9e702b1
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 107 deletions.
11 changes: 11 additions & 0 deletions src/helpers/units.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dayjs from "dayjs";
import type { Nullable } from "../types/empty";

// -- time
Expand Down Expand Up @@ -25,6 +26,16 @@ export function roundStartDate(startDate: Date): Date {
}
}

export function computeApproximateDurationSeconds(
startDate: Date | "NOW",
endDate: Date,
): number {
const startEpoch =
startDate === "NOW" ? currentEpoch() : dateToEpoch(startDate);
const endEpoch = dateToEpoch(endDate);
return dayjs(epochToDate(endEpoch)).diff(dayjs(epochToDate(startEpoch)), "s");
}

export function roundEndDate(endDate: Date): Date {
return epochToDate(roundEpochUpToHour(dateToEpoch(endDate)));
}
Expand Down
209 changes: 102 additions & 107 deletions src/lib/buy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import {
type Cents,
centsToDollarsFormatted,
computeApproximateDurationSeconds,
priceWholeToCents,
roundEndDate,
roundStartDate,
Expand Down Expand Up @@ -128,7 +129,8 @@ async function buyOrderAction(options: SfBuyOptions) {
}
}

let endDate: Date = dayjs(startDate).add(durationSeconds, "s").toDate();
let endDate: Date = dayjs(startDate === "NOW" ? new Date() : startDate).add(durationSeconds, "s").toDate();
let didQuote = false;
if (options.quote) {
const quote = await getQuote({
instanceType: options.type,
Expand All @@ -141,7 +143,9 @@ async function buyOrderAction(options: SfBuyOptions) {
return logAndQuit("Not enough data exists to quote this order.");
}

durationSeconds = dayjs(quote.end_at).diff(dayjs(quote.start_at), "s");
startDate = quote.start_at === "NOW" ? "NOW" : new Date(quote.start_at);
endDate = new Date(quote.end_at);
durationSeconds = computeApproximateDurationSeconds(startDate, endDate);

const priceLabelUsd = c.green(centsToDollarsFormatted(quote.price));
const priceLabelPerGPUHour = c.green(
Expand All @@ -155,133 +159,131 @@ async function buyOrderAction(options: SfBuyOptions) {
),
);

endDate = new Date(quote.end_at);
console.log(
`Found availability from ${c.green(quote.start_at)} to ${c.green(quote.end_at)} (${c.green(formatDuration(durationSeconds * 1000))}) at ${priceLabelUsd} total (${priceLabelPerGPUHour}/GPU-hour)`,
);
} else {
// quote if no price was provided
if (!priceCents) {
const quote = await getQuote({
instanceType: options.type,
quantity: quantity,
startsAt: startDate,
durationSeconds,
});

if (!quote) {
const durationInHours = durationSeconds / 3600;
didQuote = true;
} else if (!priceCents) {
const quote = await getQuote({
instanceType: options.type,
quantity: quantity,
startsAt: startDate,
durationSeconds,
});

console.log(`No one is selling this right now. To ask someone to sell it to you, add a price you're willing to pay. For example:
if (!quote) {
const durationInHours = durationSeconds / 3600;

sf buy -d "${durationInHours}h" -n ${quantity * GPUS_PER_NODE} -p "2.50"
`);
console.log(`No one is selling this right now. To ask someone to sell it to you, add a price you're willing to pay. For example:
return process.exit(1);
}
sf buy -d "${durationInHours}h" -n ${quantity * GPUS_PER_NODE} -p "2.50"
`);

priceCents = quote.price;
durationSeconds = dayjs(quote.end_at).diff(dayjs(quote.start_at), "s");
startDate = new Date(quote.start_at);
endDate = new Date(quote.end_at);
return process.exit(1);
}

if (!durationSeconds) {
throw new Error("unexpectly no duration provided");
}
if (!priceCents) {
throw new Error("unexpectly no price provided");
}
startDate = quote.start_at === "NOW" ? "NOW" : new Date(quote.start_at);
endDate = new Date(quote.end_at);
durationSeconds = computeApproximateDurationSeconds(startDate, endDate);
priceCents = quote.price;
didQuote = true;
}

if (!durationSeconds) {
throw new Error("unexpectly no duration provided");
}
if (!priceCents) {
throw new Error("unexpectly no price provided");
}

// round the start date if it's not "NOW". If it came from a quote, it's already rounded
// if we didn't quote, we need to round the start and end dates
if (!didQuote) {
// round the start date if it's not "NOW".
const roundedStartDate =
startDate !== "NOW" ? roundStartDate(startDate) : startDate;

// round the end date. If it came from a quote, it's already rounded
// round the end date.
const roundedEndDate = roundEndDate(endDate);

// if we rounded the time, prorate the price
const roundedDurationSeconds = dayjs(roundedEndDate).diff(
dayjs(
roundedStartDate === "NOW"
? roundStartDate(new Date())
: roundedStartDate,
),
"s",
const roundedDurationSeconds = computeApproximateDurationSeconds(
roundedStartDate,
roundedEndDate,
);
if (roundedDurationSeconds !== durationSeconds) {
const priceCentsPerSecond = priceCents / durationSeconds;
const roundedPriceCents = priceCentsPerSecond * roundedDurationSeconds;
console.log(
"Duration rounded to fit trading grid. Your price has been pro-rated to: ",
c.green(centsToDollarsFormatted(roundedPriceCents)),
);
priceCents = roundedPriceCents;
}

if (confirmWithUser) {
const confirmationMessage = confirmPlaceOrderMessage({
instanceType: options.type,
priceCents,
quantity,
startsAt: startDate,
endsAt: endDate,
confirmWithUser,
quoteOnly: isQuoteOnly,
});
const confirmed = await confirm({
message: confirmationMessage,
default: false,
});

if (!confirmed) {
logAndQuit("Order cancelled");
}
}
const priceCentsPerSecond = priceCents / durationSeconds;
const roundedPriceCents = priceCentsPerSecond * roundedDurationSeconds;

priceCents = roundedPriceCents;
startDate = roundedStartDate;
endDate = roundedEndDate;
durationSeconds = roundedDurationSeconds;
}

const res = await placeBuyOrder({
if (confirmWithUser) {
const confirmationMessage = confirmPlaceOrderMessage({
instanceType: options.type,
priceCents,
quantity,
// round start date again because the user might have taken a long time to confirm
// most of the time this will do nothing, but when it does it will move the start date forwrd one minute
startsAt: startDate === "NOW" ? "NOW" : roundStartDate(startDate),
durationSeconds,
startsAt: startDate,
endsAt: endDate,
confirmWithUser,
quoteOnly: isQuoteOnly,
});
const confirmed = await confirm({
message: confirmationMessage,
default: false,
});

const order = await waitForOrderToNotBePending(res.id);
if (!order) {
return;
if (!confirmed) {
logAndQuit("Order cancelled");
}
}

if (order.status === "filled") {
const now = new Date();
const startAt = new Date(order.start_at);
const timeDiff = startAt.getTime() - now.getTime();
const oneMinuteInMs = 60 * 1000;
const res = await placeBuyOrder({
instanceType: options.type,
priceCents,
quantity,
// round start date again because the user might have taken a long time to confirm
// most of the time this will do nothing, but when it does it will move the start date forwrd one minute
startsAt: startDate === "NOW" ? "NOW" : roundStartDate(startDate),
endsAt: endDate,
confirmWithUser,
quoteOnly: isQuoteOnly,
});

if (now >= startAt || timeDiff <= oneMinuteInMs) {
console.log(`Your nodes are currently spinning up. Once they're online, you can view them using:
const order = await waitForOrderToNotBePending(res.id);
if (!order) {
return;
}

if (order.status === "filled") {
const now = new Date();
const startAt = new Date(order.start_at);
const timeDiff = startAt.getTime() - now.getTime();
const oneMinuteInMs = 60 * 1000;

if (now >= startAt || timeDiff <= oneMinuteInMs) {
console.log(`Your nodes are currently spinning up. Once they're online, you can view them using:
sf instances ls
`);
} else {
const contractStartTime = dayjs(startAt);
const timeFromNow = contractStartTime.fromNow();
console.log(`Your contract begins ${c.green(timeFromNow)}. You can view more details using:
} else {
const contractStartTime = dayjs(startAt);
const timeFromNow = contractStartTime.fromNow();
console.log(`Your contract begins ${c.green(timeFromNow)}. You can view more details using:
sf contracts ls
`);
}
return;
}
return;
}

if (order.status === "open") {
console.log(`Your order wasn't accepted yet. You can check it's status with:
if (order.status === "open") {
console.log(`Your order wasn't accepted yet. You can check it's status with:
sf orders ls
Expand All @@ -290,15 +292,14 @@ async function buyOrderAction(options: SfBuyOptions) {
sf orders cancel ${order.id}
`);
return;
}
return;
}

console.error(`Order likely did not execute. Check the status with:
console.error(`Order likely did not execute. Check the status with:
sf orders ls
`);
}
}

function confirmPlaceOrderMessage(options: BuyOptions) {
Expand All @@ -310,19 +311,11 @@ function confirmPlaceOrderMessage(options: BuyOptions) {
const instanceTypeLabel = c.green(options.instanceType);
const nodesLabel = options.quantity > 1 ? "nodes" : "node";

const durationSeconds = dayjs(options.endsAt).diff(
dayjs(
options.startsAt === "NOW"
? roundStartDate(new Date())
: options.startsAt,
),
"s",
);
const durationHumanReadable = formatDuration(durationSeconds * 1000);
const durationHumanReadable = formatDuration(options.durationSeconds * 1000);
const endsAtLabel = c.green(
dayjs(options.endsAt).format("MM/DD/YYYY hh:mm A"),
);
const fromNowTime = dayjs(options.startsAt).fromNow();
const fromNowTime = dayjs(options.startsAt === "NOW" ? new Date() : options.startsAt).fromNow();

let timeDescription: string;
if (
Expand All @@ -332,20 +325,21 @@ function confirmPlaceOrderMessage(options: BuyOptions) {
timeDescription = `from ${c.green("now")} until ${endsAtLabel}`;
} else {
const startAtLabel = c.green(
dayjs(options.startsAt).format("MM/DD/YYYY hh:mm A"),
options.startsAt === "NOW" ? "NOW" : dayjs(options.startsAt).format("MM/DD/YYYY hh:mm A"),
);
timeDescription = `from ${startAtLabel} (${c.green(fromNowTime)}) until ${endsAtLabel}`;
}

const pricePerGPUHour = totalPriceToPricePerGPUHour(
options.priceCents,
durationSeconds,
options.durationSeconds,
options.quantity,
GPUS_PER_NODE,
);
const pricePerHourLabel = c.green(centsToDollarsFormatted(pricePerGPUHour));
const totalPriceLabel = c.green(centsToDollarsFormatted(options.priceCents));

const topLine = `${totalNodesLabel} ${instanceTypeLabel} ${nodesLabel} (${GPUS_PER_NODE * options.quantity} GPUs) at ${pricePerHourLabel} per GPU hour for ${c.green(durationHumanReadable)} ${timeDescription}`;
const topLine = `${totalNodesLabel} ${instanceTypeLabel} ${nodesLabel} (${GPUS_PER_NODE * options.quantity} GPUs) at ${pricePerHourLabel} per GPU hour for ${c.green(durationHumanReadable)} ${timeDescription} for a total of ${totalPriceLabel}`;

const dollarsLabel = c.green(centsToDollarsFormatted(pricePerGPUHour));

Expand All @@ -362,10 +356,11 @@ type BuyOptions = {
quantity: number;
startsAt: Date | "NOW";
endsAt: Date;
durationSeconds: number;
confirmWithUser: boolean;
quoteOnly: boolean;
};
export async function placeBuyOrder(options: BuyOptions) {
export async function placeBuyOrder(options: Omit<BuyOptions, "durationSeconds">) {
const api = await apiClient();
const { data, error, response } = await api.POST("/v0/orders", {
body: {
Expand Down

0 comments on commit 9e702b1

Please sign in to comment.