diff --git a/apps/vendor-ccxt/CHANGELOG.json b/apps/vendor-ccxt/CHANGELOG.json new file mode 100644 index 000000000..5808c08f3 --- /dev/null +++ b/apps/vendor-ccxt/CHANGELOG.json @@ -0,0 +1,128 @@ +{ + "name": "@yuants/vendor-ccxt", + "entries": [ + { + "version": "0.0.7", + "tag": "@yuants/vendor-ccxt_v0.0.7", + "date": "Tue, 16 Jan 2024 02:58:35 GMT", + "comments": { + "patch": [ + { + "comment": "add manifest key" + } + ], + "none": [ + { + "comment": "Bump Version" + } + ] + } + }, + { + "version": "0.0.6", + "tag": "@yuants/vendor-ccxt_v0.0.6", + "date": "Mon, 08 Jan 2024 13:52:27 GMT", + "comments": { + "none": [ + { + "comment": "Bump Version" + } + ], + "patch": [ + { + "comment": "renovate: update dependencies" + } + ] + } + }, + { + "version": "0.0.5", + "tag": "@yuants/vendor-ccxt_v0.0.5", + "date": "Fri, 22 Dec 2023 16:41:16 GMT", + "comments": { + "patch": [ + { + "comment": "fix submit order issue" + } + ], + "none": [ + { + "comment": "Bump Version" + } + ] + } + }, + { + "version": "0.0.4", + "tag": "@yuants/vendor-ccxt_v0.0.4", + "date": "Thu, 21 Dec 2023 20:44:17 GMT", + "comments": { + "patch": [ + { + "comment": "fix period channel and accountInfo channel" + }, + { + "comment": "renovate: update dependencies" + } + ], + "none": [ + { + "comment": "Bump Version" + }, + { + "comment": "Bump Version" + } + ] + } + }, + { + "version": "0.0.3", + "tag": "@yuants/vendor-ccxt_v0.0.3", + "date": "Mon, 11 Dec 2023 20:01:27 GMT", + "comments": { + "patch": [ + { + "comment": "refine implementation" + }, + { + "comment": "upgrade dependencies: '@yuants/protocol' to 0.9.3" + } + ], + "none": [ + { + "comment": "Bump Version" + } + ] + } + }, + { + "version": "0.0.2", + "tag": "@yuants/vendor-ccxt_v0.0.2", + "date": "Sun, 10 Dec 2023 16:19:55 GMT", + "comments": { + "patch": [ + { + "comment": "upgrade dependencies" + } + ], + "none": [ + { + "comment": "Bump Version" + } + ] + } + }, + { + "version": "0.0.1", + "tag": "@yuants/vendor-ccxt_v0.0.1", + "date": "Sat, 09 Dec 2023 21:48:38 GMT", + "comments": { + "patch": [ + { + "comment": "upgrade protocol" + } + ] + } + } + ] +} diff --git a/apps/vendor-ccxt/CHANGELOG.md b/apps/vendor-ccxt/CHANGELOG.md new file mode 100644 index 000000000..7e10b2c9f --- /dev/null +++ b/apps/vendor-ccxt/CHANGELOG.md @@ -0,0 +1,55 @@ +# Change Log - @yuants/vendor-ccxt + +This log was last generated on Tue, 16 Jan 2024 02:58:35 GMT and should not be manually modified. + +## 0.0.7 +Tue, 16 Jan 2024 02:58:35 GMT + +### Patches + +- add manifest key + +## 0.0.6 +Mon, 08 Jan 2024 13:52:27 GMT + +### Patches + +- renovate: update dependencies + +## 0.0.5 +Fri, 22 Dec 2023 16:41:16 GMT + +### Patches + +- fix submit order issue + +## 0.0.4 +Thu, 21 Dec 2023 20:44:17 GMT + +### Patches + +- fix period channel and accountInfo channel +- renovate: update dependencies + +## 0.0.3 +Mon, 11 Dec 2023 20:01:27 GMT + +### Patches + +- refine implementation +- upgrade dependencies: '@yuants/protocol' to 0.9.3 + +## 0.0.2 +Sun, 10 Dec 2023 16:19:55 GMT + +### Patches + +- upgrade dependencies + +## 0.0.1 +Sat, 09 Dec 2023 21:48:38 GMT + +### Patches + +- upgrade protocol + diff --git a/apps/vendor-ccxt/api-extractor.json b/apps/vendor-ccxt/api-extractor.json new file mode 100644 index 000000000..62f4fd324 --- /dev/null +++ b/apps/vendor-ccxt/api-extractor.json @@ -0,0 +1,411 @@ +/** + * Config file for API Extractor. For more info, please visit: https://api-extractor.com + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + /** + * Optionally specifies another JSON config file that this file extends from. This provides a way for + * standard settings to be shared across multiple projects. + * + * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains + * the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be + * resolved using NodeJS require(). + * + * SUPPORTED TOKENS: none + * DEFAULT VALUE: "" + */ + // "extends": "./shared/api-extractor-base.json" + // "extends": "my-package/include/api-extractor-base.json" + + /** + * Determines the "" token that can be used with other config file settings. The project folder + * typically contains the tsconfig.json and package.json config files, but the path is user-defined. + * + * The path is resolved relative to the folder of the config file that contains the setting. + * + * The default value for "projectFolder" is the token "", which means the folder is determined by traversing + * parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder + * that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error + * will be reported. + * + * SUPPORTED TOKENS: + * DEFAULT VALUE: "" + */ + // "projectFolder": "..", + + /** + * (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor + * analyzes the symbols exported by this module. + * + * The file extension must be ".d.ts" and not ".ts". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + */ + "mainEntryPointFilePath": "/lib/index.d.ts", + + /** + * A list of NPM package names whose exports should be treated as part of this package. + * + * For example, suppose that Webpack is used to generate a distributed bundle for the project "library1", + * and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part + * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly + * imports library2. To avoid this, we can specify: + * + * "bundledPackages": [ "library2" ], + * + * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been + * local files for library1. + */ + "bundledPackages": [], + + /** + * Specifies what type of newlines API Extractor should use when writing output files. By default, the output files + * will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead. + * To use the OS's default newline kind, specify "os". + * + * DEFAULT VALUE: "crlf" + */ + // "newlineKind": "crlf", + + /** + * Set to true when invoking API Extractor's test harness. When `testMode` is true, the `toolVersion` field in the + * .api.json file is assigned an empty string to prevent spurious diffs in output files tracked for tests. + * + * DEFAULT VALUE: "false" + */ + // "testMode": false, + + /** + * Specifies how API Extractor sorts members of an enum when generating the .api.json file. By default, the output + * files will be sorted alphabetically, which is "by-name". To keep the ordering in the source code, specify + * "preserve". + * + * DEFAULT VALUE: "by-name" + */ + // "enumMemberOrder": "by-name", + + /** + * Determines how the TypeScript compiler engine will be invoked by API Extractor. + */ + "compiler": { + /** + * Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * Note: This setting will be ignored if "overrideTsconfig" is used. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/tsconfig.json" + */ + // "tsconfigFilePath": "/tsconfig.json", + /** + * Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk. + * The object must conform to the TypeScript tsconfig schema: + * + * http://json.schemastore.org/tsconfig + * + * If omitted, then the tsconfig.json file will be read from the "projectFolder". + * + * DEFAULT VALUE: no overrideTsconfig section + */ + // "overrideTsconfig": { + // . . . + // } + /** + * This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended + * and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when + * dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses + * for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck. + * + * DEFAULT VALUE: false + */ + // "skipLibCheck": true, + }, + + /** + * Configures how the API report file (*.api.md) will be generated. + */ + "apiReport": { + /** + * (REQUIRED) Whether to generate an API report. + */ + "enabled": true + + /** + * The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce + * a full file path. + * + * The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/". + * + * SUPPORTED TOKENS: , + * DEFAULT VALUE: ".api.md" + */ + // "reportFileName": ".api.md", + + /** + * Specifies the folder where the API report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy, + * e.g. for an API review. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/" + */ + // "reportFolder": "/temp/", + + /** + * Specifies the folder where the temporary report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * After the temporary file is written to disk, it is compared with the file in the "reportFolder". + * If they are different, a production build will fail. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/" + */ + // "reportTempFolder": "/temp/", + + /** + * Whether "forgotten exports" should be included in the API report file. Forgotten exports are declarations + * flagged with `ae-forgotten-export` warnings. See https://api-extractor.com/pages/messages/ae-forgotten-export/ to + * learn more. + * + * DEFAULT VALUE: "false" + */ + // "includeForgottenExports": false + }, + + /** + * Configures how the doc model file (*.api.json) will be generated. + */ + "docModel": { + /** + * (REQUIRED) Whether to generate a doc model file. + */ + "enabled": true + + /** + * The output path for the doc model file. The file extension should be ".api.json". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/.api.json" + */ + // "apiJsonFilePath": "/temp/.api.json", + + /** + * Whether "forgotten exports" should be included in the doc model file. Forgotten exports are declarations + * flagged with `ae-forgotten-export` warnings. See https://api-extractor.com/pages/messages/ae-forgotten-export/ to + * learn more. + * + * DEFAULT VALUE: "false" + */ + // "includeForgottenExports": false + }, + + /** + * Configures how the .d.ts rollup file will be generated. + */ + "dtsRollup": { + /** + * (REQUIRED) Whether to generate the .d.ts rollup file. + */ + "enabled": true + + /** + * Specifies the output path for a .d.ts rollup file to be generated without any trimming. + * This file will include all declarations that are exported by the main entry point. + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/dist/.d.ts" + */ + // "untrimmedFilePath": "/dist/.d.ts", + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for an "alpha" release. + * This file will include only declarations that are marked as "@public", "@beta", or "@alpha". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "alphaTrimmedFilePath": "/dist/-alpha.d.ts", + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release. + * This file will include only declarations that are marked as "@public" or "@beta". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "betaTrimmedFilePath": "/dist/-beta.d.ts", + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release. + * This file will include only declarations that are marked as "@public". + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "publicTrimmedFilePath": "/dist/-public.d.ts", + + /** + * When a declaration is trimmed, by default it will be replaced by a code comment such as + * "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the + * declaration completely. + * + * DEFAULT VALUE: false + */ + // "omitTrimmingComments": true + }, + + /** + * Configures how the tsdoc-metadata.json file will be generated. + */ + "tsdocMetadata": { + /** + * Whether to generate the tsdoc-metadata.json file. + * + * DEFAULT VALUE: true + */ + // "enabled": true, + /** + * Specifies where the TSDoc metadata file should be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * The default value is "", which causes the path to be automatically inferred from the "tsdocMetadata", + * "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup + * falls back to "tsdoc-metadata.json" in the package folder. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "tsdocMetadataFilePath": "/dist/tsdoc-metadata.json" + }, + + /** + * Configures how API Extractor reports error and warning messages produced during analysis. + * + * There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages. + */ + "messages": { + /** + * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing + * the input .d.ts files. + * + * TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "compilerMessageReporting": { + /** + * Configures the default routing for messages that don't match an explicit rule in this table. + */ + "default": { + /** + * Specifies whether the message should be written to the the tool's output log. Note that + * the "addToApiReportFile" property may supersede this option. + * + * Possible values: "error", "warning", "none" + * + * Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail + * and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes + * the "--local" option), the warning is displayed but the build will not fail. + * + * DEFAULT VALUE: "warning" + */ + "logLevel": "warning" + + /** + * When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md), + * then the message will be written inside that file; otherwise, the message is instead logged according to + * the "logLevel" option. + * + * DEFAULT VALUE: false + */ + // "addToApiReportFile": false + } + + // "TS2551": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + }, + + /** + * Configures handling of messages reported by API Extractor during its analysis. + * + * API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag" + * + * DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings + */ + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + // "addToApiReportFile": false + } + + // "ae-extra-release-tag": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + }, + + /** + * Configures handling of messages reported by the TSDoc parser when analyzing code comments. + * + * TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + // "addToApiReportFile": false + } + + // "tsdoc-link-tag-unescaped-text": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + } + } +} diff --git a/apps/vendor-ccxt/build/Dockerfile b/apps/vendor-ccxt/build/Dockerfile new file mode 100644 index 000000000..7af8804f1 --- /dev/null +++ b/apps/vendor-ccxt/build/Dockerfile @@ -0,0 +1,26 @@ +# syntax = docker/dockerfile:1.4 + +FROM node:18.17.0-bullseye-slim + +LABEL maintainer="Siyuan Wang " + +# https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md#handling-kernel-signals +ARG TINI_VERSION=v0.19.0 +ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini +RUN chmod a+x /tini +ENTRYPOINT ["/tini", "--"] + +USER node + +WORKDIR /app + +ENV NODE_ENV=production + +COPY --chown=node:node ./out/vendor-ccxt-out /app + +RUN node create-links.js create + +WORKDIR /app/vendors/ccxt + +# USER nobody +CMD ["node", "./lib/index.js"] \ No newline at end of file diff --git a/apps/vendor-ccxt/config/jest.config.json b/apps/vendor-ccxt/config/jest.config.json new file mode 100644 index 000000000..4bb17bde3 --- /dev/null +++ b/apps/vendor-ccxt/config/jest.config.json @@ -0,0 +1,3 @@ +{ + "extends": "@rushstack/heft-node-rig/profiles/default/config/jest.config.json" +} diff --git a/apps/vendor-ccxt/config/rig.json b/apps/vendor-ccxt/config/rig.json new file mode 100644 index 000000000..f6c7b5537 --- /dev/null +++ b/apps/vendor-ccxt/config/rig.json @@ -0,0 +1,18 @@ +// The "rig.json" file directs tools to look for their config files in an external package. +// Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + + /** + * (Required) The name of the rig package to inherit from. + * It should be an NPM package name with the "-rig" suffix. + */ + "rigPackageName": "@rushstack/heft-node-rig" + + /** + * (Optional) Selects a config profile from the rig package. The name must consist of + * lowercase alphanumeric words separated by hyphens, for example "sample-profile". + * If omitted, then the "default" profile will be used." + */ + // "rigProfile": "your-profile-name" +} diff --git a/apps/vendor-ccxt/config/typescript.json b/apps/vendor-ccxt/config/typescript.json new file mode 100644 index 000000000..854907e8a --- /dev/null +++ b/apps/vendor-ccxt/config/typescript.json @@ -0,0 +1,87 @@ +/** + * Configures the TypeScript plugin for Heft. This plugin also manages linting. + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/heft/typescript.schema.json", + + /** + * Optionally specifies another JSON config file that this file extends from. This provides a way for standard + * settings to be shared across multiple projects. + */ + // "extends": "base-project/config/typescript.json", + + /** + * Can be set to "copy" or "hardlink". If set to "copy", copy files from cache. + * If set to "hardlink", files will be hardlinked to the cache location. + * This option is useful when producing a tarball of build output as TAR files don't + * handle these hardlinks correctly. "hardlink" is the default behavior. + */ + // "copyFromCacheMode": "copy", + + /** + * If provided, emit these module kinds in addition to the modules specified in the tsconfig. + * Note that this option only applies to the main tsconfig.json configuration. + */ + "additionalModuleKindsToEmit": [ + { + "moduleKind": "esnext", + "outFolderName": "dist" + } + // { + // /** + // * (Required) Must be one of "commonjs", "amd", "umd", "system", "es2015", "esnext" + // */ + // "moduleKind": "amd", + // + // /** + // * (Required) The name of the folder where the output will be written. + // */ + // "outFolderName": "lib-amd" + // } + ], + + /** + * Specifies the intermediary folder that tests will use. Because Jest uses the + * Node.js runtime to execute tests, the module format must be CommonJS. + * + * The default value is "lib". + */ + // "emitFolderNameForTests": "lib-commonjs", + + /** + * If set to "true", the TSlint task will not be invoked. + */ + // "disableTslint": true, + + /** + * Set this to change the maximum number of file handles that will be opened concurrently for writing. + * The default is 50. + */ + // "maxWriteParallelism": 50, + + /** + * Configures additional file types that should be copied into the TypeScript compiler's emit folders, for example + * so that these files can be resolved by import statements. + */ + "staticAssetsToCopy": { + /** + * File extensions that should be copied from the src folder to the destination folder(s). + */ + // "fileExtensions": [ + // ".json", ".css" + // ], + /** + * Glob patterns that should be explicitly included. + */ + // "includeGlobs": [ + // "some/path/*.js" + // ], + /** + * Glob patterns that should be explicitly excluded. This takes precedence over globs listed + * in "includeGlobs" and files that match the file extensions provided in "fileExtensions". + */ + // "excludeGlobs": [ + // "some/path/*.css" + // ] + } +} diff --git a/apps/vendor-ccxt/etc/vendor-ccxt.api.md b/apps/vendor-ccxt/etc/vendor-ccxt.api.md new file mode 100644 index 000000000..11167a257 --- /dev/null +++ b/apps/vendor-ccxt/etc/vendor-ccxt.api.md @@ -0,0 +1,9 @@ +## API Report File for "@yuants/vendor-ccxt" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// (No @packageDocumentation comment for this package) + +``` diff --git a/apps/vendor-ccxt/package.json b/apps/vendor-ccxt/package.json new file mode 100644 index 000000000..2591e1a3b --- /dev/null +++ b/apps/vendor-ccxt/package.json @@ -0,0 +1,33 @@ +{ + "name": "@yuants/vendor-ccxt", + "version": "0.0.7", + "files": [ + "dist", + "lib", + "temp" + ], + "scripts": { + "build": "heft test --clean && api-extractor run --local && yuan-toolkit post-build" + }, + "dependencies": { + "@yuants/protocol": "workspace:*", + "rxjs": "~7.5.6", + "ccxt": "~4.0.107", + "@yuants/data-model": "workspace:*" + }, + "devDependencies": { + "@microsoft/api-extractor": "~7.30.0", + "@rushstack/heft": "~0.47.5", + "@rushstack/heft-jest-plugin": "~0.3.30", + "@rushstack/heft-node-rig": "~1.10.7", + "@types/heft-jest": "1.0.3", + "@types/node": "18", + "@yuants/extension": "workspace:*", + "@yuants/tool-kit": "workspace:*", + "typescript": "~4.7.4" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org", + "access": "public" + } +} diff --git a/apps/vendor-ccxt/src/extension.ts b/apps/vendor-ccxt/src/extension.ts new file mode 100644 index 000000000..7cacf6178 --- /dev/null +++ b/apps/vendor-ccxt/src/extension.ts @@ -0,0 +1,93 @@ +import { IExtensionContext, makeDockerEnvs, makeK8sEnvs } from '@yuants/extension'; +export default (context: IExtensionContext) => { + context.registerDeployProvider({ + make_json_schema: () => ({ + type: 'object', + properties: { + env: { + type: 'object', + required: ['HOST_URL'], + properties: { + // + HOST_URL: { type: 'string' }, + TERMINAL_ID: { type: 'string' }, + EXCHANGE_ID: { type: 'string' }, + ACCOUNT_ID: { type: 'string' }, + CURRENCY: { type: 'string' }, + PASSWORD: { type: 'string' }, + API_KEY: { type: 'string' }, + SECRET: { type: 'string' }, + }, + }, + }, + }), + make_docker_compose_file: async (ctx, envCtx) => { + return { + [`ccxt-${ctx.env!.EXCHANGE_ID}-${ctx.env!.ACCOUNT_ID}`.replace(/\s/g, '')]: { + image: `ghcr.io/no-trade-no-life/vendor-ccxt:${ctx.version ?? envCtx.version}`, + restart: 'always', + + environment: makeDockerEnvs(ctx.env), + }, + }; + }, + make_k8s_resource_objects: async (ctx, envCtx) => { + const EXCHANGE_ID = ctx.env!.EXCHANGE_ID; + const ACCOUNT_ID = ctx.env!.ACCOUNT_ID; + return { + deployment: { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { + labels: { + 'y.ntnl.io/version': ctx.version ?? envCtx.version, + 'y.ntnl.io/account_id': `${ACCOUNT_ID}`, + 'y.ntnl.io/component': 'ccxt', + }, + name: `ccxt-${EXCHANGE_ID}-${ACCOUNT_ID}-${ctx.key}`.replace(/\s/g, '').toLocaleLowerCase(), + namespace: 'yuan', + }, + spec: { + replicas: 1, + selector: { + matchLabels: { + 'y.ntnl.io/component': 'ccxt', + 'y.ntnl.io/account_id': `${ACCOUNT_ID}`, + }, + }, + template: { + metadata: { + labels: { + 'y.ntnl.io/version': ctx.version ?? envCtx.version, + 'y.ntnl.io/account_id': `${ACCOUNT_ID}`, + 'y.ntnl.io/component': 'ccxt', + }, + }, + spec: { + containers: [ + { + env: makeK8sEnvs(ctx.env), + image: `ghcr.io/no-trade-no-life/vendor-ccxt:${ctx.version ?? envCtx.version}`, + imagePullPolicy: 'IfNotPresent', + name: 'ccxt', + resources: { + limits: { + cpu: ctx.cpu?.max ?? '1', + memory: ctx.memory?.max ?? '512Mi', + }, + requests: { + cpu: ctx.cpu?.min ?? '200m', + memory: ctx.memory?.min ?? '256Mi', + }, + }, + }, + ], + hostname: 'ccxt', + }, + }, + }, + }, + }; + }, + }); +}; diff --git a/apps/vendor-ccxt/src/index.ts b/apps/vendor-ccxt/src/index.ts new file mode 100644 index 000000000..1ed11ad96 --- /dev/null +++ b/apps/vendor-ccxt/src/index.ts @@ -0,0 +1,415 @@ +import { + IAccountInfo, + IOrder, + IPeriod, + IPosition, + IProduct, + OrderDirection, + OrderType, + PositionVariant, + formatTime, +} from '@yuants/data-model'; +import { Terminal } from '@yuants/protocol'; +import '@yuants/protocol/lib/services'; +import '@yuants/protocol/lib/services/order'; +import ccxt, { Exchange } from 'ccxt'; +import { + EMPTY, + bufferCount, + defer, + delayWhen, + expand, + forkJoin, + from, + lastValueFrom, + map, + mergeMap, + of, + repeat, + retry, + shareReplay, + tap, + toArray, +} from 'rxjs'; + +(async () => { + const EXCHANGE_ID = process.env.EXCHANGE_ID!; + const ACCOUNT_ID = process.env.ACCOUNT_ID!; + const CURRENCY = process.env.CURRENCY || 'USDT'; + const EARLIEST_TIMESTAMP = +(process.env.EARLIEST_TIMESTAMP || 1262304000000); + + const CCXT_PARAMS = { + apiKey: process.env.API_KEY, + secret: process.env.SECRET, + password: process.env.PASSWORD, + httpProxy: process.env.HTTP_PROXY, + // options: { defaultType: 'swap' } + }; + console.info(formatTime(Date.now()), 'init', EXCHANGE_ID, CCXT_PARAMS); + // @ts-ignore + const ex: Exchange = new ccxt[EXCHANGE_ID](CCXT_PARAMS); + + const accounts = await lastValueFrom(from(ex.loadAccounts())); + console.info(formatTime(Date.now()), 'loadAccounts', JSON.stringify(accounts)); + const accountId = accounts[0]?.id ?? ACCOUNT_ID; + console.info(formatTime(Date.now()), 'resolve account', accountId); + + const terminal_id = process.env.TERMINAL_ID || `CCXT-${EXCHANGE_ID}-${accountId}-${CURRENCY}`; + + const terminal = new Terminal(process.env.HOST_URL!, { + terminal_id, + name: `CCXT`, + }); + + const accountInfo$ = defer(() => of(0)).pipe( + mergeMap(() => { + const balance$ = from(ex.fetchBalance()); + const positions$ = from(ex.fetchPositions()).pipe( + mergeMap((positions) => positions), + map((position): IPosition => { + return { + position_id: position.id, + product_id: mapSymbolToProductId[position.symbol], + variant: position.side === 'long' ? PositionVariant.LONG : PositionVariant.SHORT, + volume: position.contracts || 0, + free_volume: position.contracts || 0, + position_price: position.entryPrice || 0, + closable_price: position.markPrice || 0, + floating_profit: position.unrealizedPnl || 0, + }; + }), + toArray(), + ); + const orders$ = from(ex.fetchOpenOrders()).pipe( + mergeMap((orders) => orders), + map((order): IOrder => { + return { + exchange_order_id: order.id, + client_order_id: order.id, + account_id: accountId, + product_id: mapSymbolToProductId[order.symbol], + type: order.type === 'limit' ? OrderType.LIMIT : OrderType.MARKET, + direction: order.side === 'sell' ? OrderDirection.OPEN_SHORT : OrderDirection.OPEN_LONG, + volume: order.amount, + timestamp_in_us: order.timestamp * 1000, + price: order.price, + traded_volume: order.amount - order.remaining, + }; + }), + toArray(), + ); + return forkJoin([balance$, positions$, orders$]).pipe( + // + map(([balance, positions, orders]): IAccountInfo => { + return { + timestamp_in_us: Date.now() * 1000, + account_id: accountId, + money: { + currency: CURRENCY, + balance: +(balance[CURRENCY]?.total ?? 0), + free: +(balance[CURRENCY]?.free ?? 0), + used: +(balance[CURRENCY]?.used ?? 0), + equity: +(balance[CURRENCY]?.total ?? 0), + profit: 0, + }, + positions, + orders, + }; + }), + ); + }), + repeat({ delay: 1000 }), + retry({ delay: 1000 }), + shareReplay(1), + ); + + terminal.provideAccountInfo(accountInfo$); + + const mapProductIdToSymbol: Record = {}; + const mapSymbolToProductId: Record = {}; + + const products$ = defer(() => ex.loadMarkets()).pipe( + mergeMap((markets) => Object.values(markets)), + tap((market) => { + console.info('Product-Symbol', market.id, market.symbol); + mapProductIdToSymbol[market.id] = market.symbol; + mapSymbolToProductId[market.symbol] = market.id; + }), + map( + (market): IProduct => ({ + datasource_id: EXCHANGE_ID, + product_id: market.id, + base_currency: market.base, + quote_currency: market.quote, + value_scale: market.contractSize, + volume_step: market.precision.amount || 1, + price_step: market.precision.price || 1, + }), + ), + toArray(), + repeat({ delay: 86400_000 }), + retry({ delay: 10_000 }), + shareReplay(1), + ); + + products$ + .pipe( + // + mergeMap((products) => terminal.updateProducts(products)), + ) + .subscribe(); + + // const mapPeriodInSecToCCXTTimeframe: Record = { + // 1: '1s', + // 60: '1m', + // 180: '3m', + // 300: '5m', + // 900: '15m', + // 1800: '30m', + // 3600: '1h', + // 7200: '2h', + // 14400: '4h', + // 21600: '6h', + // 43200: '12h', + // 86400: '1d', + // 259200: '3d', + // 604800: '1w', + // 2592000: '1M' + // }; + + const mapPeriodInSecToCCXTTimeframe = (period_in_sec: number): string => { + if (period_in_sec % 2592000 === 0) { + return `${period_in_sec / 2592000}M`; + } + if (period_in_sec % 604800 === 0) { + return `${period_in_sec / 604800}w`; + } + if (period_in_sec % 86400 === 0) { + return `${period_in_sec / 86400}d`; + } + if (period_in_sec % 3600 === 0) { + return `${period_in_sec / 3600}h`; + } + if (period_in_sec % 60 === 0) { + return `${period_in_sec / 60}m`; + } + return `${period_in_sec}s`; + }; + + terminal.provideService( + 'CopyDataRecords', + { + required: ['tags'], + properties: { + tags: { + type: 'object', + required: ['datasource_id'], + properties: { + datasource_id: { + const: EXCHANGE_ID, + }, + }, + }, + }, + }, + (msg) => { + console.info(formatTime(Date.now()), `CopyDataRecords`, JSON.stringify(msg.req)); + if (msg.req.type === 'period') { + const product_id = msg.req.tags?.product_id!; + const period_in_sec = +msg.req.tags?.period_in_sec!; + const timeframe = mapPeriodInSecToCCXTTimeframe(period_in_sec); + + const [start_timestamp, end_timestamp] = msg.req.time_range || [ + Date.now() - period_in_sec * 1000 * 100, + Date.now(), + ]; + const timeInterval = period_in_sec * 1000 * 100; + const start = Math.max(EARLIEST_TIMESTAMP, start_timestamp); + + if (product_id && period_in_sec && timeframe) { + console.info(formatTime(Date.now()), `FetchOHLCVStarted`); + return of({ + periods: [], + // ISSUE: OKX 的接口语义为两侧开区间,因此需要 -1 以包含 start_time_in_us + current_start_timestamp: start - 1, + current_end_timestamp: start + timeInterval - 1, + }).pipe( + // + tap(() => { + console.info(formatTime(Date.now()), `RecursivelyFetchOHLCVStarted`); + }), + expand(({ current_start_timestamp, current_end_timestamp }) => { + console.info( + formatTime(Date.now()), + `FetchOHLCVStarted`, + `current_start_timestamp: ${current_start_timestamp}`, + `current_end_timestamp: ${current_end_timestamp}`, + ); + if (current_start_timestamp > end_timestamp) return EMPTY; + return from( + ex.fetchOHLCV(mapProductIdToSymbol[product_id], timeframe, current_start_timestamp, 100, { + until: current_end_timestamp, + }), + ).pipe( + // + retry({ delay: 10_000 }), + tap((v) => { + console.info(formatTime(Date.now()), `FetchOHLCVFinished`, `total: ${v.length}`); + }), + mergeMap((x) => x), + map( + ([t, o, h, l, c, vol]): IPeriod => ({ + datasource_id: EXCHANGE_ID, + product_id, + period_in_sec, + timestamp_in_us: t * 1000, + open: o, + high: h, + low: l, + close: c, + volume: vol, + }), + ), + toArray(), + map((periods) => ({ + periods, + current_start_timestamp: current_start_timestamp + timeInterval, + current_end_timestamp: current_end_timestamp + timeInterval, + })), + ); + }), + mergeMap(({ periods }) => periods), + bufferCount(2000), + delayWhen((periods) => terminal.updatePeriods(periods)), + map(() => ({ res: { code: 0, message: 'OK' } })), + ); + } + } + return of({ res: { code: 400, message: 'Bad Request' } }); + }, + ); + + terminal.providePeriods(EXCHANGE_ID, (product_id, period_in_sec) => { + console.info(formatTime(Date.now()), 'period_stream', product_id, period_in_sec); + return defer(() => { + const timeframe = mapPeriodInSecToCCXTTimeframe(period_in_sec); + const symbol = mapProductIdToSymbol[product_id]; + const since = Date.now() - 3 * period_in_sec * 1000; + if (!symbol) { + return of([]); + } + return ex.fetchOHLCV(symbol, timeframe, since); + }).pipe( + mergeMap((x) => x), + map( + ([t, o, h, l, c, vol]): IPeriod => ({ + datasource_id: EXCHANGE_ID, + product_id, + period_in_sec, + timestamp_in_us: t * 1000, + start_at: t, + open: o, + high: h, + low: l, + close: c, + volume: vol, + }), + ), + toArray(), + repeat({ delay: 1000 }), + retry({ delay: 1000 }), + ); + }); + + terminal.provideService( + 'SubmitOrder', + { + required: ['account_id'], + properties: { + account_id: { + const: accountId, + }, + }, + }, + (msg) => { + console.info(formatTime(Date.now()), `SubmitOrder`, JSON.stringify(msg.req)); + const { product_id, type, direction } = msg.req; + const symbol = mapProductIdToSymbol[product_id]; + if (!symbol) { + return of({ res: { code: 400, message: 'No such symbol' } }); + } + const ccxtType = type === OrderType.MARKET ? 'market' : 'limit'; + const ccxtSide = + direction === OrderDirection.OPEN_LONG || direction === OrderDirection.CLOSE_SHORT ? 'buy' : 'sell'; + const volume = msg.req.volume; + const price = msg.req.price; + const posSide = + direction === OrderDirection.OPEN_LONG || direction === OrderDirection.CLOSE_LONG ? 'long' : 'short'; + console.info( + formatTime(Date.now()), + 'submit to ccxt', + JSON.stringify({ symbol, ccxtType, ccxtSide, volume, price, posSide }), + ); + return from( + ex.createOrder(symbol, ccxtType, ccxtSide, volume, price, { + // ISSUE: okx hedge LONG/SHORT mode need to set 'posSide' to 'long' or 'short'. + posSide: posSide, + }), + ).pipe( + map(() => { + return { res: { code: 0, message: 'OK' } }; + }), + ); + }, + ); + + terminal.provideService( + 'CancelOrder', + { + required: ['account_id'], + properties: { + account_id: { + const: accountId, + }, + }, + }, + (msg) => { + console.info(formatTime(Date.now()), `CancelOrder`, JSON.stringify(msg.req)); + return from(ex.cancelOrder(msg.req.client_order_id)).pipe( + map(() => { + return { res: { code: 0, message: 'OK' } }; + }), + ); + }, + ); + + terminal.provideService( + 'ModifyOrder', + { + required: ['account_id'], + properties: { + account_id: { + const: accountId, + }, + }, + }, + (msg) => { + console.info(formatTime(Date.now()), `ModifyOrder`, JSON.stringify(msg.req)); + const { client_order_id, product_id, type, direction } = msg.req; + const symbol = mapProductIdToSymbol[product_id]; + if (!symbol) { + return of({ res: { code: 400, message: 'No such symbol' } }); + } + const ccxtType = type === OrderType.MARKET ? 'market' : 'limit'; + const ccxtSide = + direction === OrderDirection.OPEN_LONG || direction === OrderDirection.CLOSE_SHORT ? 'buy' : 'sell'; + return from( + ex.editOrder(client_order_id, symbol, ccxtType, ccxtSide, msg.req.volume, msg.req.price), + ).pipe( + map(() => { + return { res: { code: 0, message: 'OK' } }; + }), + ); + }, + ); +})(); diff --git a/apps/vendor-ccxt/tsconfig.json b/apps/vendor-ccxt/tsconfig.json new file mode 100644 index 000000000..b22b8b6e1 --- /dev/null +++ b/apps/vendor-ccxt/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "./node_modules/@rushstack/heft-node-rig/profiles/default/tsconfig-base.json", + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "types": ["heft-jest", "node"] + } +} diff --git a/apps/vendor-deepcoin/build/Dockerfile b/apps/vendor-deepcoin/build/Dockerfile index 66f79f72f..7c66487bf 100644 --- a/apps/vendor-deepcoin/build/Dockerfile +++ b/apps/vendor-deepcoin/build/Dockerfile @@ -15,7 +15,7 @@ USER node WORKDIR /app -COPY --chown=node:node ./out/app-vendor-deepcoin-out /app +COPY --chown=node:node ./out/vendor-deepcoin-out /app RUN node create-links.js create diff --git a/common/changes/@yuants/vendor-ccxt/2024-02-20-10-23.json b/common/changes/@yuants/vendor-ccxt/2024-02-20-10-23.json new file mode 100644 index 000000000..f22e663b9 --- /dev/null +++ b/common/changes/@yuants/vendor-ccxt/2024-02-20-10-23.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@yuants/vendor-ccxt", + "comment": "init for opensource", + "type": "patch" + } + ], + "packageName": "@yuants/vendor-ccxt" +} \ No newline at end of file diff --git a/common/changes/@yuants/vendor-deepcoin/2024-02-20-10-44.json b/common/changes/@yuants/vendor-deepcoin/2024-02-20-10-44.json new file mode 100644 index 000000000..b23ad7220 --- /dev/null +++ b/common/changes/@yuants/vendor-deepcoin/2024-02-20-10-44.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@yuants/vendor-deepcoin", + "comment": "skip", + "type": "patch" + } + ], + "packageName": "@yuants/vendor-deepcoin" +} \ No newline at end of file diff --git a/common/config/rush/deploy.json b/common/config/rush/deploy.json index 629feb5a7..265714a90 100644 --- a/common/config/rush/deploy.json +++ b/common/config/rush/deploy.json @@ -30,6 +30,7 @@ "@yuants/app-general-realtime-data-source", "@yuants/app-trade-copier", "@yuants/vendor-deepcoin", + "@yuants/vendor-ccxt", "@yuants/vendor-ctp" ], /** diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index e0be34103..927d45252 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -469,6 +469,37 @@ importers: json-schema: 0.4.0 typescript: 4.7.4 + ../../apps/vendor-ccxt: + specifiers: + '@microsoft/api-extractor': ~7.30.0 + '@rushstack/heft': ~0.47.5 + '@rushstack/heft-jest-plugin': ~0.3.30 + '@rushstack/heft-node-rig': ~1.10.7 + '@types/heft-jest': 1.0.3 + '@types/node': '18' + '@yuants/data-model': workspace:* + '@yuants/extension': workspace:* + '@yuants/protocol': workspace:* + '@yuants/tool-kit': workspace:* + ccxt: ~4.0.107 + rxjs: ~7.5.6 + typescript: ~4.7.4 + dependencies: + '@yuants/data-model': link:../../libraries/data-model + '@yuants/protocol': link:../../libraries/protocol + ccxt: 4.0.112 + rxjs: 7.5.7 + devDependencies: + '@microsoft/api-extractor': 7.30.1 + '@rushstack/heft': 0.47.11 + '@rushstack/heft-jest-plugin': 0.3.45_@rushstack+heft@0.47.11 + '@rushstack/heft-node-rig': 1.10.13_@rushstack+heft@0.47.11 + '@types/heft-jest': 1.0.3 + '@types/node': 18.17.12 + '@yuants/extension': link:../../libraries/extension + '@yuants/tool-kit': link:../../tools/toolkit + typescript: 4.7.4 + ../../apps/vendor-ctp: specifiers: '@microsoft/api-extractor': ~7.30.0 @@ -7681,6 +7712,17 @@ packages: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} dev: false + /ccxt/4.0.112: + resolution: {integrity: sha512-BTecIwn3KICjvEDbxjfFLTtr996eSfR0IznoVwqZGqRpmlY0b3MB2JfxzPpwCOZ5WFCj1H12cKWo0hg5wlKpLw==} + engines: {node: '>=15.0.0'} + requiresBuild: true + dependencies: + ws: 8.14.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + /chainsaw/0.1.0: resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==} dependencies: diff --git a/rush.json b/rush.json index a0cf52f90..f0a1713cb 100644 --- a/rush.json +++ b/rush.json @@ -540,6 +540,11 @@ "projectFolder": "apps/vendor-deepcoin", "shouldPublish": true }, + { + "packageName": "@yuants/vendor-ccxt", + "projectFolder": "apps/vendor-ccxt", + "shouldPublish": true + }, { "packageName": "@yuants/ui-web", "projectFolder": "ui/web"