Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"format": "biome format --write .",
"generate-client-factory": "tsx --env-file .env scripts/generate-client-factory.ts",
"generate-routes": "tsr generate",
"generate-tradingview-symbols": "tsx --env-file .env scripts/generate-tradingview-symbols.ts"
"generate-tradingview-symbols": "tsx --env-file .env scripts/generate-tradingview-symbols/index.ts"
},
"dependencies": {
"@base-ui/react": "^1.1.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,69 +7,19 @@ import {
HttpClientRequest,
HttpClientResponse,
} from "@effect/platform";
import { Array as _Array, Config, Effect, Layer, Logger } from "effect";
import {
Array as _Array,
Config,
Data,
Effect,
Layer,
Logger,
Order,
Schema,
} from "effect";

const DEFAULT_LIMIT = 50;

const exchangePriorityRecord: Record<string, number> = {
Coinbase: 0,
Binance: 1,
CRYPTOCAP: 2,
"Crypto.com": 3,
};

// -----------------------------------------------------------------------------
// Schemas
// -----------------------------------------------------------------------------
const TokenDto = Schema.Struct({ symbol: Schema.String });

const ProviderDto = Schema.Struct({
id: Schema.String,
name: Schema.optionalWith(Schema.String, { nullable: true }),
});

const MarketDto = Schema.Struct({
id: Schema.String,
providerId: Schema.String,
baseAsset: TokenDto,
quoteAsset: TokenDto,
});

const MarketsResponse = Schema.Struct({
total: Schema.Number,
offset: Schema.Number,
limit: Schema.Number,
items: Schema.optionalWith(Schema.Array(MarketDto), { nullable: true }),
});

const ProvidersResponse = Schema.Array(ProviderDto);

const TradingViewSearchResponse = Schema.Struct({
symbols: Schema.Array(
Schema.Struct({
symbol: Schema.String,
exchange: Schema.String,
provider_id: Schema.String,
}),
),
});

// -----------------------------------------------------------------------------
// Error Types
// -----------------------------------------------------------------------------
class HttpError extends Data.TaggedError("HttpError")<{
message: string;
cause?: unknown;
}> {}
type BaseSymbolSchema,
byProviderAndCurrency,
compareSymbolsFromBaseSymbol,
DEFAULT_LIMIT,
HttpError,
MarketsResponse,
makeResult,
normalizeSymbol,
ProvidersResponse,
TradingViewSearchResponse,
} from "scripts/generate-tradingview-symbols/utils";

// -----------------------------------------------------------------------------
// HttpClient Services
Expand Down Expand Up @@ -123,11 +73,6 @@ class TradingViewClient extends Effect.Service<TradingViewClient>()(
// -----------------------------------------------------------------------------
// API Functions
// -----------------------------------------------------------------------------
const normalizeSymbol = (symbol: string) =>
symbol
.replace(/<\/?em>/g, "")
.trim()
.toUpperCase();

const getProviders = Effect.gen(function* () {
const perpsClient = yield* PerpsClient;
Expand Down Expand Up @@ -226,50 +171,23 @@ const searchTradingViewSymbol = (
),
);

const CheckTradingViewSymbolResult = Schema.Union(
Schema.Struct({
status: Schema.Literal("match"),
perpsSymbol: Schema.String,
tradingViewSymbol: Schema.String,
providerId: Schema.String,
}),
Schema.Struct({
status: Schema.Literal("noMatch"),
perpsSymbol: Schema.String,
}),
Schema.Struct({
status: Schema.Literal("error"),
perpsSymbol: Schema.String,
}),
);

const makeResult = Schema.decodeSync(CheckTradingViewSymbolResult);

const checkTradingViewSymbol = Effect.fn(function* (baseSymbol: string) {
const checkTradingViewSymbol = Effect.fn(function* (
baseSymbol: typeof BaseSymbolSchema.Type,
) {
const tvClient = yield* TradingViewClient;

const normalizedBase = normalizeSymbol(baseSymbol);

return yield* searchTradingViewSymbol(tvClient, normalizedBase).pipe(
Effect.map((response) => {
const results = _Array.sort(
response.symbols,
Order.mapInput(
Order.number,
(v: (typeof response.symbols)[number]) =>
exchangePriorityRecord[v.exchange] ?? 999,
),
);
const results = _Array.sort(response.symbols, byProviderAndCurrency);

const compareSymbols = compareSymbolsFromBaseSymbol(normalizedBase);

const matchedResult = _Array.findFirst(results, (result) => {
const resultSymbol = normalizeSymbol(result.symbol);

return [
normalizedBase,
`${normalizedBase}USD`,
`${normalizedBase}USDC`,
`${normalizedBase}USDT`,
].some((symbol) => resultSymbol === symbol);
return _Array.some(compareSymbols, (symbol) => resultSymbol === symbol);
});

if (matchedResult._tag === "Some") {
Expand Down Expand Up @@ -324,11 +242,12 @@ const program = Effect.gen(function* () {

const nonMatched = results.filter((result) => result.status !== "match");

yield* Effect.log("Non matched: ", JSON.stringify(nonMatched, null, 2));
yield* Effect.log("Not matched: ", JSON.stringify(nonMatched, null, 2));

const path = join(
dirname(fileURLToPath(import.meta.url)),
"..",
"..",
"src",
"assets",
"tradingview-symbols.json",
Expand Down
147 changes: 147 additions & 0 deletions scripts/generate-tradingview-symbols/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { Data, Option, Order, Record, Schema } from "effect";

export const DEFAULT_LIMIT = 50;

// -----------------------------------------------------------------------------
// Schemas
// -----------------------------------------------------------------------------
export const ProviderId = Schema.String.pipe(Schema.brand("ProviderId"));

export const CompareCurrencySchema = Schema.Literal("USD", "USDC", "USDT");

export const currencyPriorityRecord: Record<
typeof CompareCurrencySchema.Type,
number
> = {
USD: 0,
USDC: 1,
USDT: 2,
};

export const BaseSymbolSchema = Schema.String.pipe(Schema.brand("BaseSymbol"));

export const TokenDto = Schema.Struct({ symbol: BaseSymbolSchema });

export const ProviderDto = Schema.Struct({
id: Schema.String,
name: Schema.optionalWith(Schema.String, { nullable: true }),
});

export const MarketDto = Schema.Struct({
id: Schema.String,
providerId: Schema.String,
baseAsset: TokenDto,
quoteAsset: TokenDto,
});

export const MarketsResponse = Schema.Struct({
total: Schema.Number,
offset: Schema.Number,
limit: Schema.Number,
items: Schema.optionalWith(Schema.Array(MarketDto), { nullable: true }),
});

export const ProvidersResponse = Schema.Array(ProviderDto);

export const TradingViewSearchResponse = Schema.Struct({
symbols: Schema.Array(
Schema.Struct({
symbol: Schema.String, // e.g. ETHUSD
exchange: Schema.String,
provider_id: ProviderId,
}),
),
});

const CheckTradingViewSymbolResult = Schema.Union(
Schema.Struct({
status: Schema.Literal("match"),
perpsSymbol: Schema.String,
tradingViewSymbol: Schema.String,
providerId: Schema.String,
}),
Schema.Struct({
status: Schema.Literal("noMatch"),
perpsSymbol: Schema.String,
}),
Schema.Struct({
status: Schema.Literal("error"),
perpsSymbol: Schema.String,
}),
);

// -----------------------------------------------------------------------------
// Error Types
// -----------------------------------------------------------------------------
export class HttpError extends Data.TaggedError("HttpError")<{
message: string;
cause?: unknown;
}> {}

// -----------------------------------------------------------------------------
// Utilities
// -----------------------------------------------------------------------------

export const exchangePriorityRecord: Record<typeof ProviderId.Type, number> = {
[ProviderId.make("coinbase")]: 0,
[ProviderId.make("binance")]: 1,
[ProviderId.make("cryptocom")]: 2,
[ProviderId.make("kraken")]: 3,
[ProviderId.make("okx")]: 4,
[ProviderId.make("cryptocap")]: 5,
};

export const normalizeSymbol = <T extends string>(symbol: T): T =>
symbol
.replace(/<\/?em>/g, "")
.trim()
.toUpperCase() as T;

export const makeResult = Schema.decodeSync(CheckTradingViewSymbolResult);

const byCurrency = Order.mapInput(
Order.number,
(val: (typeof TradingViewSearchResponse.Type)["symbols"][number]) => {
const compareSymbol = Schema.decodeUnknownOption(
Schema.TemplateLiteralParser(
Schema.String,
Schema.Literal("USD", "USDC", "USDT"),
),
)(val.symbol).pipe(
Option.map((v) => v[1]),
Option.getOrElse(() => null),
);

if (!compareSymbol) return 999;

return Record.get(currencyPriorityRecord, compareSymbol).pipe(
Option.getOrElse(() => 999),
);
},
);

const byProvider = Order.mapInput(
Order.number,
(val: (typeof TradingViewSearchResponse.Type)["symbols"][number]) =>
Record.get(exchangePriorityRecord, val.provider_id).pipe(
Option.getOrElse(() => 999),
),
);

export const byProviderAndCurrency = Order.combine(byCurrency, byProvider);

export const compareSymbolsFromBaseSymbol = (
baseSymbol: typeof BaseSymbolSchema.Type,
) => {
const compareSymbols = Schema.decodeSync(
Schema.Array(
Schema.TemplateLiteral(BaseSymbolSchema, CompareCurrencySchema),
),
)(
CompareCurrencySchema.literals.map(
(currency) => `${baseSymbol}${currency}` as const,
),
);

return [...compareSymbols, baseSymbol];
};
Loading