├── .gitignore ├── settings.gradle.kts ├── examples ├── plugin-basic │ ├── settings.gradle.kts │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── app │ │ ├── build.gradle.kts │ │ └── src │ │ │ └── main │ │ │ └── kotlin │ │ │ └── com │ │ │ └── example │ │ │ └── basic │ │ │ └── App.kt │ ├── README.md │ ├── gradlew.bat │ └── gradlew └── library-basic │ ├── settings.gradle.kts │ ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── app │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ └── com │ │ └── example │ │ └── basic │ │ └── App.kt │ ├── README.md │ ├── gradlew.bat │ └── gradlew ├── lib ├── gradle.properties ├── module.md ├── src │ └── main │ │ └── kotlin │ │ ├── module-info.java │ │ └── org │ │ └── rationalityfrontline │ │ └── ktrader │ │ └── broker │ │ └── ctp │ │ ├── CtpBrokerPlugin.kt │ │ ├── CtpBrokerExtension.kt │ │ ├── CtpBrokerInfo.kt │ │ ├── CtpConfig.kt │ │ ├── utils.kt │ │ ├── CtpBrokerApi.kt │ │ ├── CtpMdApi.kt │ │ └── Converter.kt └── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew.bat ├── gradlew ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gradle 3 | build 4 | plugins 5 | data 6 | CtpAccounts.kt -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "ktrader-broker-ctp" 2 | include("lib") -------------------------------------------------------------------------------- /examples/plugin-basic/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "plugin-basic" 2 | include("app") 3 | -------------------------------------------------------------------------------- /examples/library-basic/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "library-basic" 2 | include("app") 3 | -------------------------------------------------------------------------------- /lib/gradle.properties: -------------------------------------------------------------------------------- 1 | # See https://youtrack.jetbrains.com/issue/KT-45545 2 | kapt.use.worker.api=false -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktrader-tech/ktrader-broker-ctp/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /lib/module.md: -------------------------------------------------------------------------------- 1 | # Module KTrader-Broker-CTP 2 | 3 | [KTrader-Broker-API](https://github.com/ktrader-tech/ktrader-broker-api) 的 CTP 实现 4 | -------------------------------------------------------------------------------- /examples/plugin-basic/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktrader-tech/ktrader-broker-ctp/HEAD/examples/plugin-basic/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /examples/library-basic/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktrader-tech/ktrader-broker-ctp/HEAD/examples/library-basic/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /examples/library-basic/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /examples/plugin-basic/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /examples/plugin-basic/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") version "1.5.30" 3 | application 4 | } 5 | 6 | repositories { 7 | mavenCentral() 8 | mavenLocal() 9 | } 10 | 11 | dependencies { 12 | implementation("org.rationalityfrontline.workaround:pf4j:3.7.0") 13 | implementation("org.rationalityfrontline.ktrader:ktrader-broker-api:1.2.0") 14 | } 15 | 16 | application { 17 | mainClass.set("com.example.basic.AppKt") 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/main/kotlin/module-info.java: -------------------------------------------------------------------------------- 1 | @SuppressWarnings("requires-transitive-automatic") 2 | module ktrader.broker.ctp { 3 | requires transitive kotlin.stdlib; 4 | requires transitive kotlinx.coroutines.core.jvm; 5 | requires transitive kevent; 6 | requires transitive ktrader.datatype; 7 | requires transitive ktrader.broker.api; 8 | requires jctp; 9 | requires static org.pf4j; 10 | 11 | exports org.rationalityfrontline.ktrader.broker.ctp; 12 | } -------------------------------------------------------------------------------- /examples/library-basic/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") version "1.5.30" 3 | application 4 | } 5 | 6 | repositories { 7 | mavenCentral() 8 | mavenLocal() 9 | } 10 | 11 | dependencies { 12 | implementation("org.rationalityfrontline.ktrader:ktrader-broker-ctp:1.2.0") 13 | // 如果需要使用其它版本的 JCTP,取消注释下面一行,并填入自己需要的版本号 14 | // implementation("org.rationalityfrontline:jctp") { version { strictly("6.6.1_P1_CP-1.0.1") } } 15 | } 16 | 17 | application { 18 | mainClass.set("com.example.basic.AppKt") 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/main/kotlin/org/rationalityfrontline/ktrader/broker/ctp/CtpBrokerPlugin.kt: -------------------------------------------------------------------------------- 1 | package org.rationalityfrontline.ktrader.broker.ctp 2 | 3 | import org.pf4j.Plugin 4 | import org.pf4j.PluginWrapper 5 | import org.rationalityfrontline.jctp.jctpJNI 6 | 7 | class CtpBrokerPlugin(wrapper: PluginWrapper) : Plugin(wrapper) { 8 | override fun start() { 9 | // 这是为了防止插件在未被使用即被 delete 时 jctpJNI.release() 报错 10 | jctpJNI.libraryLoaded() 11 | } 12 | 13 | override fun delete() { 14 | // 释放 native gc root 15 | jctpJNI.release() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/library-basic/README.md: -------------------------------------------------------------------------------- 1 | # Library-Example-Basic 2 | 展示以类库方式使用 [KTrader-Broker-CTP](https://github.com/ktrader-tech/ktrader-broker-ctp) 的最简单基础的示例项目。 3 | 4 | ## 运行 5 | 1. 将 ./app/src/main/kotlin/com/example/basic/App.kt 中的 config 中的内容替换为你自己的测试账号的登录参数。 6 | 2. 在当前目录下运行命令 `.\gradlew run`(Windows) 或 `./gradlew run`(Linux) 。 7 | 8 | 在一开始你会看到以下输出信息,这与日志打印相关,不影响项目运行。在类库依赖中添加一个 [SLF4J](http://www.slf4j.org/) 支持的日志类库即可消除该信息。 9 | ```text 10 | SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder". 11 | SLF4J: Defaulting to no-operation (NOP) logger implementation 12 | SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details. 13 | ``` -------------------------------------------------------------------------------- /examples/plugin-basic/README.md: -------------------------------------------------------------------------------- 1 | # Plugin-Example-Basic 2 | 展示以插件方式使用 [KTrader-Broker-CTP](https://github.com/ktrader-tech/ktrader-broker-ctp) 的最简单基础的示例项目。 3 | 4 | ## 运行 5 | 1. 新建目录 ./app/plugins,并将需要测试的插件 ZIP 放到该目录下。 6 | 2. 将 ./app/src/main/kotlin/com/example/basic/App.kt 中的 config 中的内容替换为你自己的测试账号的登录参数。 7 | 3. 在当前目录下运行命令 `.\gradlew run`(Windows) 或 `./gradlew run`(Linux) 。 8 | 9 | 在一开始你会看到以下输出信息,这与日志打印相关,不影响项目运行。在类库依赖中添加一个 [SLF4J](http://www.slf4j.org/) 支持的日志类库即可消除该信息。 10 | ```text 11 | SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder". 12 | SLF4J: Defaulting to no-operation (NOP) logger implementation 13 | SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details. 14 | ``` -------------------------------------------------------------------------------- /lib/src/main/kotlin/org/rationalityfrontline/ktrader/broker/ctp/CtpBrokerExtension.kt: -------------------------------------------------------------------------------- 1 | package org.rationalityfrontline.ktrader.broker.ctp 2 | 3 | import org.pf4j.Extension 4 | import org.rationalityfrontline.kevent.KEvent 5 | import org.rationalityfrontline.ktrader.broker.api.BrokerApi 6 | import org.rationalityfrontline.ktrader.broker.api.BrokerExtension 7 | 8 | @Extension 9 | class CtpBrokerExtension : BrokerExtension() { 10 | override val name: String = CtpBrokerInfo.name 11 | override val version: String = CtpBrokerInfo.version 12 | override val configKeys: List> = CtpBrokerInfo.configKeys 13 | override val methodExtras: List> = CtpBrokerInfo.methodExtras 14 | 15 | override fun createApi(config: Map, kEvent: KEvent): BrokerApi { 16 | return CtpBrokerApi(CtpConfig.fromMap(config), kEvent) 17 | } 18 | } -------------------------------------------------------------------------------- /lib/src/main/kotlin/org/rationalityfrontline/ktrader/broker/ctp/CtpBrokerInfo.kt: -------------------------------------------------------------------------------- 1 | package org.rationalityfrontline.ktrader.broker.ctp 2 | 3 | import org.rationalityfrontline.jctp.CThostFtdcTraderApi 4 | 5 | /** 6 | * 记录了 CtpBroker 的相关信息(初始化参数、额外参数等) 7 | */ 8 | object CtpBrokerInfo { 9 | 10 | /** 11 | * 交易接口名称 12 | */ 13 | const val name: String = "CTP" 14 | 15 | /** 16 | * 交易接口版本 17 | */ 18 | val version: String = CThostFtdcTraderApi.GetApiVersion() 19 | 20 | /** 21 | * 实例化 CtpBrokerApi 时所需的参数说明。Pair.first 为参数名,Pair.second 为参数说明。 例:Pair("password", "String 投资者资金账号的密码") 22 | */ 23 | val configKeys: List> = listOf( 24 | Pair("mdFronts", "List 行情前置"), 25 | Pair("tdFronts", "List 交易前置"), 26 | Pair("investorId", "String 投资者资金账号"), 27 | Pair("password", "String 投资者资金账号的密码"), 28 | Pair("brokerId", "String 经纪商ID"), 29 | Pair("appId", "String 交易终端软件的标识码"), 30 | Pair("authCode", "String 交易终端软件的授权码"), 31 | Pair("userProductInfo", "String 交易终端软件的产品信息"), 32 | Pair("cachePath", "String 存贮订阅信息文件等临时文件的目录"), 33 | Pair("disableAutoSubscribe", "Boolean 是否禁止自动订阅持仓合约的行情(用于计算合约今仓保证金以及查询持仓时返回最新价及盈亏)"), 34 | Pair("disableFeeCalculation", "Boolean 是否禁止计算保证金及手续费(首次计算某个合约的费用时,可能会查询该合约的最新 Tick、保证金率、手续费率,造成额外开销,后续再次计算时则会使用上次查询的结果)"), 35 | ) 36 | 37 | /** 38 | * CtpBrokerApi 成员方法的额外参数(extras: Map?)说明。Pair.first 为方法名,Pair.second 为额外参数说明。 39 | */ 40 | val methodExtras: List> = listOf( 41 | Pair("subscribeMarketData/unsubscribeMarketData/subscribeAllMarketData/unsubscribeAllMarketData", "[isForce: Boolean = false]【是否强制向交易所发送未更改的订阅请求(默认只发送未/已被订阅的标的的订阅请求)】"), 42 | Pair("querySecurity", "[queryFee: Boolean = false]【是否查询保证金率及手续费率,如果之前没查过,可能会耗时。当 useCache 为 false 时无效】"), 43 | ) 44 | } -------------------------------------------------------------------------------- /lib/src/main/kotlin/org/rationalityfrontline/ktrader/broker/ctp/CtpConfig.kt: -------------------------------------------------------------------------------- 1 | package org.rationalityfrontline.ktrader.broker.ctp 2 | 3 | /** 4 | * CtpBrokerApi 的实例化参数 5 | * 6 | * @param mdFronts 行情前置 7 | * @param tdFronts 交易前置 8 | * @param investorId 投资者资金账号 9 | * @param password 投资者资金账号的密码 10 | * @param brokerId 经纪商ID 11 | * @param appId 交易终端软件的标识码 12 | * @param authCode 交易终端软件的授权码 13 | * @param userProductInfo 交易终端软件的产品信息 14 | * @param cachePath 存贮订阅信息文件等临时文件的目录 15 | * @param disableAutoSubscribe 是否禁止自动订阅持仓合约的行情(用于计算合约今仓保证金以及查询持仓时返回最新价及盈亏) 16 | * @param disableFeeCalculation 是否禁止计算保证金及手续费(首次计算某个合约的费用时,可能会查询该合约的最新 Tick、保证金率、手续费率,造成额外开销,后续再次计算时则会使用上次查询的结果) 17 | */ 18 | data class CtpConfig( 19 | val mdFronts: List, 20 | val tdFronts: List, 21 | val investorId: String, 22 | val password: String, 23 | val brokerId: String, 24 | val appId: String, 25 | val authCode: String, 26 | val userProductInfo: String, 27 | val cachePath: String, 28 | val disableAutoSubscribe: Boolean = false, 29 | val disableFeeCalculation: Boolean = false, 30 | ) { 31 | companion object { 32 | /** 33 | * 将标准的 Map 格式的 config 转换为 CtpConfig 34 | */ 35 | fun fromMap(config: Map): CtpConfig { 36 | return CtpConfig( 37 | mdFronts = config["mdFronts"]?.run { subSequence(1, length - 1).split(", ") } ?: listOf(), 38 | tdFronts = config["tdFronts"]?.run { subSequence(1, length - 1).split(", ") } ?: listOf(), 39 | investorId = config["investorId"] ?: "", 40 | password = config["password"] ?: "", 41 | brokerId = config["brokerId"] ?: "", 42 | appId = config["appId"] ?: "", 43 | authCode = config["authCode"] ?: "", 44 | userProductInfo = config["userProductInfo"] ?: "", 45 | cachePath = config["cachePath"] ?: "", 46 | disableAutoSubscribe = config["disableAutoSubscribe"] == "true", 47 | disableFeeCalculation = config["disableFeeCalculation"] == "true", 48 | ) 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /examples/plugin-basic/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /examples/library-basic/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /examples/library-basic/app/src/main/kotlin/com/example/basic/App.kt: -------------------------------------------------------------------------------- 1 | package com.example.basic 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import org.rationalityfrontline.kevent.KEVENT 5 | import org.rationalityfrontline.ktrader.broker.api.BrokerEvent 6 | import org.rationalityfrontline.ktrader.broker.api.BrokerEventType 7 | import org.rationalityfrontline.ktrader.broker.ctp.CtpBrokerApi 8 | import org.rationalityfrontline.ktrader.broker.ctp.CtpConfig 9 | import org.rationalityfrontline.ktrader.datatype.Direction 10 | import org.rationalityfrontline.ktrader.datatype.OrderOffset 11 | import org.rationalityfrontline.ktrader.datatype.OrderType 12 | import org.rationalityfrontline.ktrader.datatype.Tick 13 | 14 | fun main() { 15 | println("------------ 启动 ------------") 16 | // 创建 CTP 配置参数 17 | val config = CtpConfig( 18 | mdFronts = listOf( // 行情前置地址 19 | "tcp://0.0.0.0:0", 20 | ), 21 | tdFronts = listOf( // 交易前置地址 22 | "tcp://0.0.0.0:0", 23 | ), 24 | investorId = "123456", // 资金账号 25 | password = "123456", // 资金账号密码 26 | brokerId = "1234", // BROKER ID 27 | appId = "rf_ktrader_1.0.0", // APPID 28 | authCode = "ASDFGHJKL", // 授权码 29 | userProductInfo = "", // 产品信息 30 | cachePath = "./data/ctp", // 本地缓存文件存储目录 31 | disableAutoSubscribe = false, // 是否禁用自动订阅 32 | disableFeeCalculation = false, // 是否禁用费用计算 33 | ) 34 | // 创建 CtpBrokerApi 实例 35 | val api = CtpBrokerApi(config, KEVENT) 36 | println(api.version) 37 | // 订阅所有事件 38 | KEVENT.subscribeMultiple(BrokerEventType.values().asList()) { event -> runBlocking { 39 | // 处理事件推送 40 | val brokerEvent = event.data 41 | when (brokerEvent.type) { 42 | // Tick 推送 43 | BrokerEventType.TICK -> { 44 | val tick = brokerEvent.data as Tick 45 | // 当某合约触及涨停价时,以跌停价挂1手多单开仓限价委托单 46 | if (tick.lastPrice == tick.todayHighLimitPrice) { 47 | api.insertOrder(tick.code, tick.todayLowLimitPrice, 1, Direction.LONG, OrderOffset.OPEN, OrderType.LIMIT) 48 | } 49 | } 50 | // 其它事件(网络连接、订单回报、成交回报等) 51 | else -> { 52 | println(brokerEvent) 53 | } 54 | } 55 | }} 56 | // 测试 api 57 | runBlocking { 58 | api.connect() 59 | println("CTP 已连接") 60 | println("当前交易日:${api.getTradingDay()}") 61 | println("查询账户资金:") 62 | println(api.queryAssets()) 63 | println("查询账户持仓:") 64 | println(api.queryPositions().joinToString("\n")) 65 | println("查询当日全部订单:") 66 | println(api.queryOrders(onlyUnfinished = false).joinToString("\n")) 67 | println("查询当日全部成交记录:") 68 | println(api.queryTrades().joinToString("\n")) 69 | // 订阅行情 70 | api.subscribeTick("SHFE.ru2201") 71 | // Thread.currentThread().join() // 如果需要 7x24 小时不间断运行,取消注释此行。(如需主动退出运行请使用 System.exit(0) 或 exitProcess(0)) 72 | api.close() 73 | println("CTP 已关闭") 74 | } 75 | // 清空 KEVENT 76 | KEVENT.clear() 77 | println("------------ 退出 ------------") 78 | } 79 | -------------------------------------------------------------------------------- /examples/plugin-basic/app/src/main/kotlin/com/example/basic/App.kt: -------------------------------------------------------------------------------- 1 | package com.example.basic 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import org.pf4j.DefaultPluginManager 5 | import org.rationalityfrontline.kevent.KEVENT 6 | import org.rationalityfrontline.ktrader.broker.api.BrokerEvent 7 | import org.rationalityfrontline.ktrader.broker.api.BrokerEventType 8 | import org.rationalityfrontline.ktrader.broker.api.BrokerExtension 9 | import org.rationalityfrontline.ktrader.datatype.Tick 10 | import java.nio.file.Path 11 | 12 | fun main() { 13 | println("------------ 启动 ------------") 14 | val deleteOnFinish = false // 是否运行完后删除插件,可以用来测试是否存在内存泄露 15 | val pluginManager = DefaultPluginManager(Path.of("./plugins/")) 16 | pluginManager.addPluginStateListener { event -> 17 | println("插件状态变更:${event.plugin.pluginId} (${event.plugin.pluginPath}), ${event.oldState} -> ${event.pluginState}") 18 | } 19 | println("加载插件...") 20 | pluginManager.loadPlugins() 21 | println("启用插件...") 22 | pluginManager.startPlugins() 23 | println("调用插件...") 24 | pluginManager.getExtensions(BrokerExtension::class.java).forEach { brokerExtension -> 25 | if (brokerExtension.name != "CTP") return@forEach 26 | // 创建 CTP 配置参数 27 | val config = mutableMapOf( 28 | "mdFronts" to listOf("tcp://0.0.0.0:0").toString(), // 行情前置地址 29 | "tdFronts" to listOf("tcp://0.0.0.0:0").toString(), // 交易前置地址 30 | "investorId" to "123456", // 资金账号 31 | "password" to "123456", // 资金账号密码 32 | "brokerId" to "1234", // BROKER ID 33 | "appId" to "rf_ktrader_1.0.0", // APPID 34 | "authCode" to "ASDFGHJKL", // 授权码 35 | "cachePath" to "./data/ctp", // 本地缓存文件存储目录 36 | "disableAutoSubscribe" to "false", // 是否禁用自动订阅 37 | "disableFeeCalculation" to "false", // 是否禁用费用计算 38 | ) 39 | // 创建 CtpBrokerApi 实例 40 | val api = brokerExtension.createApi(config, KEVENT) 41 | println(api.version) 42 | // 订阅所有事件 43 | KEVENT.subscribeMultiple(BrokerEventType.values().asList(), tag = api.sourceId) { event -> runBlocking { 44 | // 处理事件推送 45 | val brokerEvent = event.data 46 | when (brokerEvent.type) { 47 | // Tick 推送 48 | BrokerEventType.TICK -> { 49 | val tick = brokerEvent.data as Tick 50 | println("Tick 推送:${tick.code}, ${tick.lastPrice}, ${tick.time}") 51 | } 52 | // 其它事件(网络连接、订单回报、成交回报等) 53 | else -> { 54 | println(brokerEvent) 55 | } 56 | } 57 | }} 58 | // 测试 api 59 | runBlocking { 60 | api.connect() 61 | println("CTP 已连接") 62 | println("当前交易日:${api.getTradingDay()}") 63 | println("查询账户资金:") 64 | println(api.queryAssets()) 65 | api.close() 66 | println("CTP 已关闭") 67 | } 68 | // 退订事件 69 | KEVENT.removeSubscribersByTag(api.sourceId) 70 | } 71 | if (deleteOnFinish) { 72 | println("删除插件...") 73 | pluginManager.plugins.map { it.pluginId }.forEach { 74 | pluginManager.deletePlugin(it) 75 | } 76 | } else { 77 | println("停用插件...") 78 | pluginManager.stopPlugins() 79 | } 80 | println("卸载插件...") 81 | pluginManager.unloadPlugins() 82 | println("------------ 退出 ------------") 83 | } 84 | -------------------------------------------------------------------------------- /lib/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") version "1.5.30" 3 | kotlin("kapt") version "1.5.30" 4 | `java-library` 5 | `maven-publish` 6 | signing 7 | id("org.jetbrains.dokka") version "1.5.0" 8 | id("org.javamodularity.moduleplugin") version "1.8.8" 9 | } 10 | 11 | group = "org.rationalityfrontline.ktrader" 12 | version = "1.2.0" 13 | val NAME = "ktrader-broker-ctp" 14 | val DESC = "KTrader-Broker-API 的 CTP 实现" 15 | val GITHUB_REPO = "ktrader-tech/ktrader-broker-ctp" 16 | 17 | val pluginClass = "org.rationalityfrontline.ktrader.broker.ctp.CtpBrokerPlugin" 18 | val pluginId = "CTP" 19 | val pluginVersion = version as String 20 | val pluginRequires = "1.2.0" 21 | val pluginDescription = DESC 22 | val pluginProvider = "RationalityFrontline" 23 | val pluginLicense = "Apache License 2.0" 24 | 25 | repositories { 26 | mavenCentral() 27 | mavenLocal() 28 | } 29 | 30 | dependencies { 31 | val publishMaven = true // 是否发布到 Maven 仓库 32 | val depPf4j = "org.rationalityfrontline.workaround:pf4j:3.7.0" 33 | val depKTraderBrokerApi = "org.rationalityfrontline.ktrader:ktrader-broker-api:$pluginRequires" 34 | val depJCTP = "org.rationalityfrontline:jctp:6.6.1_P1-1.0.1" 35 | if (publishMaven) { // 发布到 Maven 仓库 36 | api(depKTraderBrokerApi) 37 | compileOnly(depPf4j) 38 | implementation(depJCTP) 39 | } else { // 发布为 ZIP 插件 40 | compileOnly(kotlin("stdlib")) 41 | compileOnly(depKTraderBrokerApi) 42 | compileOnly(depPf4j) 43 | kapt(depPf4j) 44 | implementation(depJCTP) { exclude(group = "org.slf4j", module = "slf4j-api") } 45 | } 46 | } 47 | 48 | kotlin { 49 | // See https://youtrack.jetbrains.com/issue/KT-45545 50 | kotlinDaemonJvmArgs = listOf("--illegal-access=permit") 51 | } 52 | 53 | sourceSets.main { 54 | java.srcDir("src/main/kotlin") 55 | } 56 | 57 | tasks { 58 | dokkaHtml { 59 | outputDirectory.set(buildDir.resolve("javadoc")) 60 | moduleName.set("KTrader-Broker-CTP") 61 | dokkaSourceSets { 62 | named("main") { 63 | includes.from("module.md") 64 | } 65 | } 66 | } 67 | register("javadocJar") { 68 | archiveClassifier.set("javadoc") 69 | from(dokkaHtml) 70 | } 71 | register("sourcesJar") { 72 | archiveClassifier.set("sources") 73 | from(sourceSets["main"].allSource) 74 | } 75 | register("pluginZip") { 76 | group = "distribution" 77 | archiveFileName.set("$pluginId-$pluginVersion.zip") 78 | into("classes") { 79 | with(jar.get()) 80 | } 81 | into("lib") { 82 | from(configurations.runtimeClasspath.get().filter { it.name.endsWith("jar") }) 83 | } 84 | } 85 | jar { 86 | manifest.attributes(mapOf( 87 | "Implementation-Title" to NAME, 88 | "Implementation-Version" to project.version, 89 | "Implementation-Vendor" to "RationalityFrontline", 90 | "Plugin-Class" to pluginClass, 91 | "Plugin-Id" to pluginId, 92 | "Plugin-Version" to pluginVersion, 93 | "Plugin-Requires" to pluginRequires, 94 | "Plugin-Description" to pluginDescription, 95 | "Plugin-Provider" to pluginProvider, 96 | "Plugin-License" to pluginLicense 97 | )) 98 | } 99 | } 100 | 101 | publishing { 102 | publications { 103 | create("maven") { 104 | from(components["java"]) 105 | artifact(tasks["sourcesJar"]) 106 | artifact(tasks["javadocJar"]) 107 | pom { 108 | name.set(NAME) 109 | description.set(DESC) 110 | artifactId = NAME 111 | packaging = "jar" 112 | url.set("https://github.com/$GITHUB_REPO") 113 | licenses { 114 | license { 115 | name.set("The Apache Software License, Version 2.0") 116 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 117 | } 118 | } 119 | developers { 120 | developer { 121 | name.set("RationalityFrontline") 122 | email.set("rationalityfrontline@gmail.com") 123 | organization.set("ktrader-tech") 124 | organizationUrl.set("https://github.com/ktrader-tech") 125 | } 126 | } 127 | scm { 128 | connection.set("scm:git:git://github.com/$GITHUB_REPO.git") 129 | developerConnection.set("scm:git:ssh://github.com:$GITHUB_REPO.git") 130 | url.set("https://github.com/$GITHUB_REPO/tree/master") 131 | } 132 | } 133 | } 134 | } 135 | repositories { 136 | fun env(propertyName: String): String { 137 | return if (project.hasProperty(propertyName)) { 138 | project.property(propertyName) as String 139 | } else "Unknown" 140 | } 141 | maven { 142 | val releasesRepoUrl = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/") 143 | val snapshotsRepoUrl = uri("https://oss.sonatype.org/content/repositories/snapshots/") 144 | url = if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl 145 | credentials { 146 | username = env("ossrhUsername") 147 | password = env("ossrhPassword") 148 | } 149 | } 150 | } 151 | } 152 | 153 | signing { 154 | sign(publishing.publications["maven"]) 155 | } 156 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /examples/library-basic/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /examples/plugin-basic/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /lib/src/main/kotlin/org/rationalityfrontline/ktrader/broker/ctp/utils.kt: -------------------------------------------------------------------------------- 1 | package org.rationalityfrontline.ktrader.broker.ctp 2 | 3 | import kotlinx.coroutines.CancellableContinuation 4 | import kotlinx.coroutines.delay 5 | import kotlinx.coroutines.suspendCancellableCoroutine 6 | import kotlinx.coroutines.withTimeout 7 | import org.rationalityfrontline.jctp.CThostFtdcRspInfoField 8 | import org.rationalityfrontline.ktrader.datatype.* 9 | import kotlin.coroutines.Continuation 10 | 11 | /** 12 | * 请求超时时间,单位:毫秒 13 | */ 14 | internal const val TIMEOUT_MILLS: Long = 6000 15 | 16 | /** 17 | * 验证 code 是否规范,并解析返回其交易所代码和合约代码([Pair.first] 为 exchangeId,[Pair.second] 为 instrumentId) 18 | */ 19 | fun parseCode(code: String): Pair { 20 | val splitResult = code.split('.', limit = 2) 21 | if (splitResult.size != 2) throw IllegalArgumentException("code 需要包含交易所信息,例:SHFE.ru2109") 22 | return Pair(splitResult[0], splitResult[1]) 23 | } 24 | 25 | /** 26 | * 检查 CTP 回调中的 [pRspInfo] 是否表明请求成功,如果成功执行 [onSuccess], 否则执行 [onError](参数为错误码及错误信息) 27 | */ 28 | internal inline fun checkRspInfo(pRspInfo: CThostFtdcRspInfoField?, onSuccess: () -> Unit, onError: (Int, String) -> Unit) { 29 | if (pRspInfo == null || pRspInfo.errorID == 0) { 30 | onSuccess() 31 | } else { 32 | onError(pRspInfo.errorID, pRspInfo.errorMsg) 33 | } 34 | } 35 | 36 | /** 37 | * 获取 CTP 发送请求时与 [errorCode] 相对应的错误信息 38 | */ 39 | internal fun getErrorInfo(errorCode: Int): String { 40 | return when (errorCode) { 41 | -1 -> "网络连接失败" 42 | -2 -> "未处理请求超过许可数" 43 | -3 -> "每秒发送请求数超过许可数" 44 | else -> "发生未知错误:$errorCode" 45 | } 46 | } 47 | 48 | /** 49 | * 获取 CTP 网络断开时与 [reason] 对应的断开原因,[reason] 为 0 时表明是主动请求断开的 50 | */ 51 | internal fun getDisconnectReason(reason: Int): String { 52 | return when(reason) { 53 | 0 -> "主动断开" 54 | 4097 -> "网络读失败" 55 | 4098 -> "网络写失败" 56 | 8193 -> "接收心跳超时" 57 | 8194 -> "发送心跳失败" 58 | 8195 -> "收到错误报文" 59 | else -> "未知原因" 60 | } 61 | } 62 | 63 | /** 64 | * 发送 CTP 请求并检查其返回码,如果请求成功执行 [onSuccess], 否则执行 [onError](参数为错误码及错误信息,默认实现为抛出异常) 65 | * @param action 包含 CTP 请求操作的方法,返回请求码 66 | * @param retry 是否在请求码为 -2 或 -3 时不断自动间隔 10ms 重新请求 67 | */ 68 | internal suspend inline fun runWithResultCheck(action: () -> Int, onSuccess: () -> T, onError: (Int, String) -> T = { code, info -> throw Exception("$info ($code)") }, retry: Boolean = true): T { 69 | var resultCode = action() 70 | if (retry) { 71 | while (resultCode == -2 || resultCode == -3) { 72 | delay(10) 73 | resultCode = action() 74 | } 75 | } 76 | return if (resultCode == 0) { 77 | onSuccess() 78 | } else { 79 | onError(resultCode, getErrorInfo(resultCode)) 80 | } 81 | } 82 | 83 | /** 84 | * 发送用 [runWithResultCheck] 包装过后的 CTP 请求,如果遇到 CTP 柜台处流控,则不断自动间隔 10ms 重新请求 85 | * @param action 用 [runWithResultCheck] 包装过后的 CTP 请求,返回的是请求结果 86 | */ 87 | internal suspend fun runWithRetry(action: suspend () -> T, onError: (Exception) -> T = { e -> throw e }): T { 88 | return try { 89 | action() 90 | } catch (e: Exception) { 91 | if (e.message == "CTP:查询未就绪,请稍后重试") { 92 | delay(10) 93 | runWithRetry(action, onError) 94 | } else { 95 | onError(e) 96 | } 97 | } 98 | } 99 | 100 | /** 101 | * [withTimeout] 与 [suspendCancellableCoroutine] 的结合简写 102 | */ 103 | internal suspend inline fun suspendCoroutineWithTimeout(timeMills: Long, crossinline block: (CancellableContinuation) -> Unit): T { 104 | return withTimeout(timeMills) { 105 | suspendCancellableCoroutine(block) 106 | } 107 | } 108 | 109 | /** 110 | * 协程请求续体,用于记录请求并在异步回调时恢复请求 111 | * @param tag 标签,主要用于登录等没有 requestId 的情况 112 | * @param data 额外数据 113 | */ 114 | internal data class RequestContinuation( 115 | val requestId: Int, 116 | val continuation: Continuation<*>, 117 | val tag: String = "", 118 | val data: Any = Unit, 119 | ) 120 | 121 | /** 122 | * 用于记录成交记录查询请求的请求参数以及保存查询的结果 123 | */ 124 | internal data class QueryTradesData( 125 | val tradeId: String? = null, 126 | var code: String? = null, 127 | var orderSysId: String? = null, 128 | val results: MutableList = mutableListOf() 129 | ) 130 | 131 | /** 132 | * 用于记录订单记录查询请求的参数以及保存查询的结果 133 | */ 134 | internal data class QueryOrdersData( 135 | val orderId: String? = null, 136 | val code: String? = null, 137 | val onlyUnfinished: Boolean = false, 138 | val results: MutableList = mutableListOf(), 139 | ) 140 | 141 | /** 142 | * 用于记录持仓明细查询请求的参数以及保存查询的结果 143 | */ 144 | internal data class QueryPositionDetailsData( 145 | val code: String? = null, 146 | val direction: Direction? = null, 147 | val results: MutableList = mutableListOf() 148 | ) 149 | 150 | /** 151 | * Order 的扩展字段,存储于 extras 中。格式为 exchangeId_orderSysId 152 | */ 153 | var Order.orderSysId: String 154 | get() = extras?.get("orderSysId") ?: "" 155 | set(value) { 156 | if (extras == null) { 157 | extras = mutableMapOf() 158 | } 159 | extras!!["orderSysId"] = value 160 | } 161 | 162 | /** 163 | * Order 的扩展字段,以 String 格式存储于 extras 中。标记该 order 是否计算过挂单费用(仅限中金所) 164 | */ 165 | var Order.insertFeeCalculated: Boolean 166 | get() = (extras?.get("insertFeeCalculated") ?: "false").toBoolean() 167 | set(value) { 168 | if (extras == null) { 169 | extras = mutableMapOf() 170 | } 171 | extras!!["insertFeeCalculated"] = value.toString() 172 | } 173 | 174 | /** 175 | * Order 的扩展字段,以 String 格式存储于 extras 中。标记该 order 是否计算过撤单费用(仅限中金所) 176 | */ 177 | var Order.cancelFeeCalculated: Boolean 178 | get() = (extras?.get("cancelFeeCalculated") ?: "false").toBoolean() 179 | set(value) { 180 | if (extras == null) { 181 | extras = mutableMapOf() 182 | } 183 | extras!!["cancelFeeCalculated"] = value.toString() 184 | } 185 | 186 | /** 187 | * 按挂单价从低到高的顺序插入 [order] 188 | */ 189 | internal fun MutableList.insert(order: Order) { 190 | var i = indexOfFirst { it.price >= order.price } 191 | i = if (i == -1) size else i 192 | add(i, order) 193 | } 194 | 195 | /** 196 | * 交易所 ID 197 | */ 198 | @Suppress("unused") 199 | object ExchangeID { 200 | const val SHFE = "SHFE" 201 | const val INE = "INE" 202 | const val CFFEX = "CFFEX" 203 | const val DCE = "DCE" 204 | const val CZCE = "CZCE" 205 | } 206 | 207 | /** 208 | * 期货保证金/期权权利金价格类型 209 | */ 210 | internal enum class MarginPriceType { 211 | /** 212 | * 昨结算价 213 | */ 214 | PRE_SETTLEMENT_PRICE, 215 | /** 216 | * 最新价 217 | */ 218 | LAST_PRICE, 219 | /** 220 | * 今日成交均价 221 | */ 222 | TODAY_SETTLEMENT_PRICE, 223 | /** 224 | * 开仓价 225 | */ 226 | OPEN_PRICE, 227 | /** 228 | * max(昨结算价, 最新价) 229 | */ 230 | MAX_PRE_SETTLEMENT_PRICE_LAST_PRICE, 231 | } -------------------------------------------------------------------------------- /lib/src/main/kotlin/org/rationalityfrontline/ktrader/broker/ctp/CtpBrokerApi.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("JoinDeclarationAndAssignment") 2 | 3 | package org.rationalityfrontline.ktrader.broker.ctp 4 | 5 | import org.rationalityfrontline.kevent.KEvent 6 | import org.rationalityfrontline.ktrader.broker.api.* 7 | import org.rationalityfrontline.ktrader.datatype.* 8 | import java.time.LocalDate 9 | 10 | /** 11 | * [BrokerApi] 的 CTP 实现 12 | */ 13 | class CtpBrokerApi(val config: CtpConfig, override val kEvent: KEvent) : BrokerApi { 14 | 15 | private val mdApi: CtpMdApi 16 | private val tdApi: CtpTdApi 17 | 18 | override val name: String = CtpBrokerInfo.name 19 | override val version: String = CtpBrokerInfo.version 20 | override val account: String = this.config.investorId 21 | override val mdConnected: Boolean get() = mdApi.connected 22 | override val tdConnected: Boolean get() = tdApi.connected 23 | 24 | init { 25 | mdApi = CtpMdApi(this.config, kEvent, sourceId) 26 | tdApi = CtpTdApi(this.config, kEvent, sourceId) 27 | mdApi.tdApi = tdApi 28 | tdApi.mdApi = mdApi 29 | } 30 | 31 | /** 32 | * 向 [kEvent] 发送一条 [BrokerEvent] 33 | */ 34 | private fun postBrokerEvent(type: BrokerEventType, data: Any) { 35 | kEvent.post(type, BrokerEvent(type, sourceId, data)) 36 | } 37 | 38 | /** 39 | * 向 [kEvent] 发送一条 [BrokerEvent].[LogEvent] 40 | */ 41 | private fun postBrokerLogEvent(level: LogLevel, msg: String) { 42 | postBrokerEvent(BrokerEventType.LOG, LogEvent(level, msg)) 43 | } 44 | 45 | override suspend fun connect(extras: Map?) { 46 | postBrokerLogEvent(LogLevel.INFO, "【CtpBrokerApi.connect】开始连接") 47 | if (!mdConnected) mdApi.connect() 48 | if (!tdConnected) tdApi.connect() 49 | postBrokerLogEvent(LogLevel.INFO, "【CtpBrokerApi.connect】连接成功") 50 | } 51 | 52 | override fun close() { 53 | postBrokerLogEvent(LogLevel.INFO, "【CtpBrokerApi.close】开始关闭") 54 | tdApi.close() 55 | mdApi.close() 56 | postBrokerLogEvent(LogLevel.INFO, "【CtpBrokerApi.close】关闭成功") 57 | } 58 | 59 | override fun getTradingDay(): LocalDate { 60 | val tradingDay = when { 61 | mdConnected -> mdApi.getTradingDay() 62 | tdConnected -> tdApi.getTradingDay() 63 | else -> null 64 | } 65 | return if (tradingDay == null) { 66 | throw Exception("行情前置与交易前置均不可用,无法获取当前交易日") 67 | } else { 68 | Converter.dateC2A(tradingDay) 69 | } 70 | } 71 | 72 | override suspend fun subscribeTicks(codes: Collection, extras: Map?) { 73 | mdApi.subscribeMarketData(codes, extras) 74 | } 75 | 76 | override suspend fun unsubscribeTicks(codes: Collection, extras: Map?) { 77 | mdApi.unsubscribeMarketData(codes, extras) 78 | } 79 | 80 | override suspend fun subscribeAllTicks(extras: Map?) { 81 | mdApi.subscribeAllMarketData(extras) 82 | } 83 | 84 | override suspend fun unsubscribeAllTicks(extras: Map?) { 85 | mdApi.unsubscribeAllMarketData(extras) 86 | } 87 | 88 | override suspend fun subscribeTick(code: String, extras: Map?) { 89 | subscribeTicks(listOf(code), extras) 90 | } 91 | 92 | override suspend fun unsubscribeTick(code: String, extras: Map?) { 93 | unsubscribeTicks(listOf(code), extras) 94 | } 95 | 96 | /** 97 | * [useCache] 无效,总是查询本地维护的数据,CTP 无此查询接口 98 | */ 99 | override suspend fun queryTickSubscriptions(useCache: Boolean, extras: Map?): List { 100 | return mdApi.querySubscriptions(useCache, extras) 101 | } 102 | 103 | override suspend fun queryLastTick(code: String, useCache: Boolean, extras: Map?): Tick? { 104 | return runWithRetry({ tdApi.queryLastTick(code, useCache, extras) }) 105 | } 106 | 107 | override suspend fun querySecurity(code: String, useCache: Boolean, extras: Map?): SecurityInfo? { 108 | return runWithRetry({ tdApi.queryInstrument(code, useCache, extras) }) 109 | } 110 | 111 | override suspend fun queryAllSecurities(useCache: Boolean, extras: Map?): List { 112 | return runWithRetry({ tdApi.queryAllInstruments(useCache, extras) }) 113 | } 114 | 115 | override suspend fun insertOrder( 116 | code: String, 117 | price: Double, 118 | volume: Int, 119 | direction: Direction, 120 | offset: OrderOffset, 121 | orderType: OrderType, 122 | minVolume: Int, 123 | extras: Map? 124 | ): Order { 125 | return tdApi.insertOrder(code, price, volume, direction, offset, orderType, minVolume, extras) 126 | } 127 | 128 | override suspend fun cancelOrder(orderId: String, extras: Map?) { 129 | tdApi.cancelOrder(orderId, extras) 130 | } 131 | 132 | override suspend fun cancelAllOrders(extras: Map?) { 133 | queryOrders(onlyUnfinished = true).forEach { cancelOrder(it.orderId) } 134 | } 135 | 136 | override suspend fun queryOrder(orderId: String, useCache: Boolean, extras: Map?): Order? { 137 | return runWithRetry({ tdApi.queryOrder(orderId, useCache, extras) }) 138 | } 139 | 140 | override suspend fun queryOrders(code: String?, onlyUnfinished: Boolean, useCache: Boolean, extras: Map?): List { 141 | return runWithRetry({ tdApi.queryOrders(code, onlyUnfinished, useCache, extras) }) 142 | } 143 | 144 | override suspend fun queryTrade(tradeId: String, useCache: Boolean, extras: Map?): Trade? { 145 | return runWithRetry({ tdApi.queryTrade(tradeId, useCache, extras) }) 146 | } 147 | 148 | override suspend fun queryTrades(code: String?, orderId: String?, useCache: Boolean, extras: Map?): List { 149 | return runWithRetry({ tdApi.queryTrades(code, orderId, useCache, extras) }) 150 | } 151 | 152 | override suspend fun queryAssets(useCache: Boolean, extras: Map?): Assets { 153 | return runWithRetry({ tdApi.queryAssets(useCache, extras) }) 154 | } 155 | 156 | override suspend fun queryPosition(code: String, direction: Direction, useCache: Boolean, extras: Map?): Position? { 157 | return runWithRetry({ tdApi.queryPosition(code, direction, useCache, extras) }) 158 | } 159 | 160 | override suspend fun queryPositionDetails(code: String, direction: Direction, useCache: Boolean, extras: Map?): PositionDetails? { 161 | return runWithRetry({ tdApi.queryPositionDetails(code, direction, useCache, extras) }) 162 | } 163 | 164 | override suspend fun queryPositionDetails(code: String?, useCache: Boolean, extras: Map?): List { 165 | return runWithRetry({ tdApi.queryPositionDetails(code, useCache, extras) }) 166 | } 167 | 168 | override suspend fun queryPositions(code: String?, useCache: Boolean, extras: Map?): List { 169 | return runWithRetry({ tdApi.queryPositions(code, useCache, extras) }) 170 | } 171 | 172 | override suspend fun prepareFeeCalculation(codes: Collection?, extras: Map?) { 173 | tdApi.prepareFeeCalculation(codes, extras) 174 | } 175 | 176 | override fun calculatePosition(position: Position, extras: Map?) { 177 | tdApi.calculatePosition(position, extras = extras) 178 | } 179 | 180 | override fun calculateOrder(order: Order, extras: Map?) { 181 | tdApi.calculateOrder(order, extras) 182 | } 183 | 184 | override fun calculateTrade(trade: Trade, extras: Map?) { 185 | tdApi.calculateTrade(trade, extras) 186 | } 187 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KTrader-Broker-CTP 2 | [![Maven Central](https://img.shields.io/maven-central/v/org.rationalityfrontline.ktrader/ktrader-broker-ctp.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22org.rationalityfrontline.ktrader%22%20AND%20a:%22ktrader-broker-ctp%22) 3 | ![platform](https://img.shields.io/badge/platform-windows%7Clinux-green) 4 | [![Apache License 2.0](https://img.shields.io/github/license/ktrader-tech/ktrader-broker-ctp)](https://github.com/ktrader-tech/ktrader-broker-ctp/blob/master/LICENSE) 5 | 6 | [KTrader-Broker-API](https://github.com/ktrader-tech/ktrader-broker-api) 的 CTP 实现。可以作为类库使用,也可以作为插件使用。 7 | 8 | 对底层 CTP 的调用使用了 CTP 的 Java 封装 [JCTP](https://github.com/ktrader-tech/jctp) ,支持 64 位的 Windows 及 Linux 操作系统。 9 | 默认使用的 JCTP 版本为 `6.6.1_P1-1.0.0`,如果需要更换为其它版本,请参考 [Download](#download) 部分。 10 | > 虽然该项目是为 [KTrader 量化交易系统](https://github.com/ktrader-tech/ktrader) 而开发的,但也可以脱离 KTrader 独立使用 11 | 12 | ## 功能特性 13 | * 利用 [Kotlin 协程](https://github.com/Kotlin/kotlinx.coroutines) 将 CTP 的异步接口封装为 [KTrader-Broker-API](https://github.com/ktrader-tech/ktrader-broker-api) 的统一同步调用方式,降低心智负担,提升开发效率 14 | * 内置自成交风控,存在自成交风险的下单请求会本地拒单 15 | * 内置撤单数量风控,单合约日内撤单数达到 499 次后会本地拒绝该合约的后续撤单请求 16 | * 内置 CTP 流控处理,调用层无需关注任何 CTP 流控信息 17 | * 内置维护本地持仓、订单、成交、Tick 缓存,让查询请求快速返回,不受流控阻塞 18 | * 支持期货及期权的交易(目前尚不支持期权行权及自对冲,仅支持期权交易) 19 | * 自动查询账户真实的手续费率(包括中金所申报手续费)与保证金率,并计算持仓、订单、成交相关的手续费、保证金、冻结资金(期权也支持) 20 | * 封装提供了一些 CTP 原生不支持的功能,如查询当前已订阅行情,Tick 中带有合约交易状态及 Tick 内成交量成交额等 21 | * 网络断开重连时会自动订阅原先已订阅的行情,不用手动重新订阅 22 | * 众所周知,CTP 使用繁琐(如登录流程)且存在很多的坑(如批量订阅行情每34个订阅会丢失一个订阅),但本框架封装后暴露给终端用户的接口是简洁且统一的 23 | * 支持 7x24 小时不间断运行 24 | 25 | ## 快速入门 26 | 这里以类库的使用方式为例,首先参考 [Download](#download) 部分添加类库依赖,然后就可以使用本框架了: 27 | ```kotlin 28 | import kotlinx.coroutines.runBlocking 29 | import org.rationalityfrontline.kevent.KEVENT 30 | import org.rationalityfrontline.ktrader.broker.api.* 31 | import org.rationalityfrontline.ktrader.broker.ctp.CtpBrokerApi 32 | 33 | fun main() { 34 | println("------------ 启动 ------------") 35 | // 创建 CTP 配置参数 36 | val config = mutableMapOf( 37 | "mdFronts" to listOf( // 行情前置地址 38 | "tcp://0.0.0.0:0", 39 | ), 40 | "tdFronts" to listOf( // 交易前置地址 41 | "tcp://0.0.0.0:0", 42 | ), 43 | "investorId" to "123456", // 资金账号 44 | "password" to "123456", // 资金账号密码 45 | "brokerId" to "1234", // BROKER ID 46 | "appId" to "rf_ktrader_1.0.0", // APPID 47 | "authCode" to "ASDFGHJKL", // 授权码 48 | "cachePath" to "./data/ctp", // 本地缓存文件存储目录 49 | "disableAutoSubscribe" to false, // 是否禁用自动订阅 50 | "disableFeeCalculation" to false, // 是否禁用费用计算 51 | ) 52 | // 创建 CtpBrokerApi 实例 53 | val api = CtpBrokerApi(config, KEVENT) 54 | // 订阅所有事件 55 | KEVENT.subscribeMultiple(BrokerEventType.values().asList()) { event -> runBlocking { 56 | // 处理事件推送 57 | val brokerEvent = event.data 58 | when (brokerEvent.type) { 59 | // Tick 推送 60 | BrokerEventType.TICK -> { 61 | val tick = brokerEvent.data as Tick 62 | // 当某合约触及涨停价时,以跌停价挂1手多单开仓限价委托单 63 | if (tick.lastPrice == tick.todayHighLimitPrice) { 64 | api.insertOrder(tick.code, tick.todayLowLimitPrice, 1, Direction.LONG, OrderOffset.OPEN, OrderType.LIMIT) 65 | } 66 | } 67 | // 其它事件(网络连接、订单回报、成交回报等) 68 | else -> { 69 | println(brokerEvent) 70 | } 71 | } 72 | }} 73 | // 测试 api 74 | runBlocking { 75 | api.connect() 76 | println("CTP 已连接") 77 | println("当前交易日:${api.getTradingDay()}") 78 | println("查询账户资金:") 79 | println(api.queryAssets()) 80 | println("查询账户持仓:") 81 | println(api.queryPositions().joinToString("\n")) 82 | println("查询当日全部订单:") 83 | println(api.queryOrders(onlyUnfinished = false).joinToString("\n")) 84 | println("查询当日全部成交记录:") 85 | println(api.queryTrades().joinToString("\n")) 86 | // 订阅行情 87 | api.subscribeMarketData("SHFE.ru2109") 88 | // Thread.currentThread().join() // 如果需要 7x24 小时不间断运行,取消注释此行。(如需主动退出运行请使用 System.exit(0) 或 exitProcess(0)) 89 | api.close() 90 | println("CTP 已关闭") 91 | } 92 | // 清空 KEVENT 93 | KEVENT.clear() 94 | println("------------ 退出 ------------") 95 | } 96 | ``` 97 | 98 | ## 示例项目 99 | 本框架在 examples 目录下提供了一些示例项目帮助使用者快速入门及创建新项目: 100 | * [library-basic](https://github.com/ktrader-tech/ktrader-broker-ctp/tree/master/examples/library-basic) :展示以类库方式使用本框架的最简单基础的示例项目 101 | * [plugin-basic](https://github.com/ktrader-tech/ktrader-broker-ctp/tree/master/examples/plugin-basic) :展示以插件方式使用本框架的最简单基础的示例项目 102 | 103 | ## 使用说明 104 | 相较于 [KTrader-Broker-API](https://github.com/ktrader-tech/ktrader-broker-api) 标准接口,本框架需要说明的参数及额外扩展如下: 105 | ```text 106 | configKeys: 107 | { 108 | mdFronts: List 行情前置 109 | tdFronts: List 交易前置 110 | investorId: String 投资者资金账号 111 | password: String 投资者资金账号的密码 112 | brokerId: String 经纪商ID 113 | appId: String 交易终端软件的标识码 114 | authCode: String 交易终端软件的授权码 115 | userProductInfo: String 交易终端软件的产品信息 116 | cachePath: String 存贮订阅信息文件等临时文件的目录 117 | disableAutoSubscribe: Boolean 是否禁止自动订阅持仓合约的行情(用于计算合约今仓保证金以及查询持仓时返回最新价及盈亏) 118 | disableFeeCalculation: Boolean 是否禁止计算保证金及手续费(首次计算某个合约的费用时,可能会查询该合约的最新 Tick、保证金率、手续费率,造成额外开销,后续再次计算时则会使用上次查询的结果) 119 | } 120 | methodExtras: 121 | { 122 | subscribeMarketData/unsubscribeMarketData/subscribeAllMarketData/unsubscribeAllMarketData: [isForce: Boolean = false]【是否强制向交易所发送未更改的订阅请求(默认只发送未/已被订阅的标的的订阅请求)】 123 | insertOrder: [minVolume: Int]【最小成交量。仅当下单类型为 OrderType.FAK 时生效】 124 | querySecurity: [queryFee: Boolean = false]【是否查询保证金率及手续费率,如果之前没查过,可能会耗时。当 useCache 为 false 时无效】 125 | } 126 | customMethods: 127 | null 128 | customEvents: 129 | null 130 | ``` 131 | 132 | 关于证券代码,统一格式为“交易所代码.合约代码”。如 "SHFE.ru2109" 表示上期所的橡胶2109合约。全部交易所如下: 133 | 134 | | 交易所 | 交易所代码 | 合约代码大小写 | 示例代码 | 135 | |------|-------|---------|--------------| 136 | | 中金所 | CFFEX | 大写 | CFFEX.IF2109 | 137 | | 上期所 | SHFE | 小写 | SHFE.ru2109 | 138 | | 能源中心 | INE | 小写 | INE.sc2109 | 139 | | 大商所 | DCE | 小写 | DCE.m2109 | 140 | | 郑商所 | CZCE | 大写 | CZCE.MA109 | 141 | 142 | ## Download 143 | 144 | **Gradle:** 145 | 146 | ```kotlin 147 | repositories { 148 | mavenCentral() 149 | } 150 | 151 | dependencies { 152 | implementation("org.rationalityfrontline.ktrader:ktrader-broker-ctp:1.1.3") 153 | // 如果需要使用其它版本的 JCTP,取消注释下面一行,并填入自己需要的版本号 154 | // implementation("org.rationalityfrontline:jctp") { version { strictly("6.6.1_P1_CP-1.0.0") } } 155 | } 156 | ``` 157 | 158 | **Maven:** 159 | 160 | ```xml 161 | 162 | org.rationalityfrontline.ktrader 163 | ktrader-broker-ctp 164 | 1.1.3 165 | 166 | ``` 167 | 168 | **插件下载:** 169 | 170 | [Releases](https://github.com/ktrader-tech/ktrader-broker-ctp/releases) 171 | 172 | 如果需要使用其它版本的 JCTP,将插件压缩包内 lib 目录下的 jctp-xxxx.jar 替换成你所需要的版本的 jar 即可。 173 | 174 | JCTP jar 下载地址:[Maven Repository](https://repo1.maven.org/maven2/org/rationalityfrontline/jctp/) 175 | 176 | ## License 177 | 178 | KTrader-Broker-CTP is released under the [Apache 2.0 license](https://github.com/ktrader-tech/ktrader-broker-ctp/blob/master/LICENSE). 179 | 180 | ``` 181 | Copyright 2021 RationalityFrontline 182 | 183 | Licensed under the Apache License, Version 2.0 (the "License"); 184 | you may not use this file except in compliance with the License. 185 | You may obtain a copy of the License at 186 | 187 | http://www.apache.org/licenses/LICENSE-2.0 188 | 189 | Unless required by applicable law or agreed to in writing, software 190 | distributed under the License is distributed on an "AS IS" BASIS, 191 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 192 | See the License for the specific language governing permissions and 193 | limitations under the License. 194 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /lib/src/main/kotlin/org/rationalityfrontline/ktrader/broker/ctp/CtpMdApi.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNCHECKED_CAST", "UNUSED_PARAMETER", "MemberVisibilityCanBePrivate", "CanBeParameter") 2 | 3 | package org.rationalityfrontline.ktrader.broker.ctp 4 | 5 | import kotlinx.coroutines.GlobalScope 6 | import kotlinx.coroutines.launch 7 | import kotlinx.coroutines.runBlocking 8 | import org.rationalityfrontline.jctp.* 9 | import org.rationalityfrontline.kevent.KEvent 10 | import org.rationalityfrontline.ktrader.broker.api.* 11 | import org.rationalityfrontline.ktrader.datatype.Tick 12 | import java.io.File 13 | import java.util.concurrent.ConcurrentHashMap 14 | import java.util.concurrent.atomic.AtomicInteger 15 | import kotlin.coroutines.Continuation 16 | import kotlin.coroutines.resume 17 | import kotlin.coroutines.resumeWithException 18 | import kotlin.coroutines.suspendCoroutine 19 | import kotlin.math.min 20 | 21 | internal class CtpMdApi(val config: CtpConfig, val kEvent: KEvent, val sourceId: String) { 22 | private val mdApi: CThostFtdcMdApi 23 | private val mdSpi: CtpMdSpi 24 | /** 25 | * 协程请求列表,每当网络断开(OnFrontDisconnected)时会清空(resumeWithException) 26 | */ 27 | private val requestMap: ConcurrentHashMap = ConcurrentHashMap() 28 | /** 29 | * 自增的请求 id,每当网络连接时(OnFrontConnected)重置为 0 30 | */ 31 | private val requestId: AtomicInteger = AtomicInteger(0) 32 | private fun nextRequestId(): Int = requestId.incrementAndGet() 33 | /** 34 | * 上次更新的交易日。当 [connected] 处于 false 状态时可能因过期而失效 35 | */ 36 | private var tradingDay: String = "" 37 | /** 38 | * 交易 Api 对象,用于获取合约的乘数、状态 39 | */ 40 | lateinit var tdApi: CtpTdApi 41 | /** 42 | * 是否已调用过 [CThostFtdcMdApi.Init] 43 | */ 44 | private var inited = false 45 | /** 46 | * 行情前置是否已连接 47 | */ 48 | private var frontConnected: Boolean = false 49 | /** 50 | * 是否已完成登录操作(即处于可用状态) 51 | */ 52 | var connected: Boolean = false 53 | private set 54 | /** 55 | * 当前交易日内已订阅的合约代码集合(当交易日发生更替时上一交易日的订阅会自动失效清零) 56 | */ 57 | val subscriptions: MutableSet = mutableSetOf() 58 | /** 59 | * 缓存的合约代码列表,key 为 InstrumentID, value 为 ExchangeID.InstrumentID(因为 OnRtnDepthMarketData 返回的数据中没有 ExchangeID,所以需要在订阅时缓存完整代码,在 CtpTdApi 获取到全合约信息时会被填充) 60 | */ 61 | val codeMap = mutableMapOf() 62 | /** 63 | * 缓存的 [Tick] 表,key 为 code,value 为 [Tick]。每当网络断开(OnFrontDisconnected)时会清空以防止出现过期缓存被查询使用的情况。当某个合约退订时,该合约的缓存 Tick 也会清空。 64 | */ 65 | val lastTicks = mutableMapOf() 66 | 67 | init { 68 | val cachePath = config.cachePath.ifBlank { "./data/ctp/" } 69 | val mdCachePath = "${if (cachePath.endsWith('/')) cachePath else "$cachePath/"}${config.investorId.ifBlank { "unknown" }}/md/" 70 | File(mdCachePath).mkdirs() 71 | mdApi = CThostFtdcMdApi.CreateFtdcMdApi(mdCachePath) 72 | mdSpi = CtpMdSpi() 73 | mdApi.RegisterSpi(mdSpi) 74 | config.mdFronts.forEach { mdFront -> 75 | mdApi.RegisterFront(mdFront) 76 | } 77 | } 78 | 79 | /** 80 | * 依据 [instrumentId] 获取完整的代码(ExchangeID.InstrumentID) 81 | */ 82 | private fun getCode(instrumentId: String): String { 83 | return codeMap[instrumentId] ?: instrumentId 84 | } 85 | 86 | /** 87 | * 将符合 [predicate] 条件的标签为 [tag] 的协程请求用 [result] 正常完成 88 | */ 89 | private fun resumeRequests(tag: String, result: T, predicate: ((RequestContinuation) -> Boolean)? = null) { 90 | requestMap.values.filter { it.tag == tag }.forEach { req -> 91 | if (predicate?.invoke(req) != false) { 92 | (req.continuation as Continuation).resume(result) 93 | requestMap.remove(req.requestId) 94 | } 95 | } 96 | } 97 | 98 | /** 99 | * 将符合 [predicate] 条件的标签为 [tag] 的协程请求用 [errorInfo] 的报错信息异常完成 100 | */ 101 | private fun resumeRequestsWithException(tag: String, errorInfo: String, predicate: ((RequestContinuation) -> Boolean)? = null) { 102 | requestMap.values.filter { it.tag == tag }.forEach { req -> 103 | if (predicate?.invoke(req) != false) { 104 | req.continuation.resumeWithException(Exception(errorInfo)) 105 | requestMap.remove(req.requestId) 106 | } 107 | } 108 | } 109 | 110 | /** 111 | * 向 [kEvent] 发送一条 [BrokerEvent] 112 | */ 113 | private fun postBrokerEvent(type: BrokerEventType, data: Any) { 114 | kEvent.post(type, BrokerEvent(type, sourceId, data)) 115 | } 116 | 117 | /** 118 | * 向 [kEvent] 发送一条 [BrokerEvent].[LogEvent] 119 | */ 120 | private fun postBrokerLogEvent(level: LogLevel, msg: String) { 121 | postBrokerEvent(BrokerEventType.LOG, LogEvent(level, msg)) 122 | } 123 | 124 | /** 125 | * 向 [kEvent] 发送一条 [BrokerEvent].[ConnectionEvent] 126 | */ 127 | private fun postBrokerConnectionEvent(msgType: ConnectionEventType, msg: String = "") { 128 | postBrokerEvent(BrokerEventType.CONNECTION, ConnectionEvent(msgType, msg)) 129 | } 130 | 131 | /** 132 | * 连接行情前置并自动完成登录。在无法连接至前置的情况下可能会长久阻塞。 133 | * 该操作不可加超时限制,因为可能在双休日等非交易时间段启动程序。 134 | */ 135 | suspend fun connect() { 136 | if (inited) return 137 | suspendCoroutine { continuation -> 138 | val requestId = Int.MIN_VALUE // 因为 OnFrontConnected 中 requestId 会重置为 0,为防止 requestId 重复,取整数最小值 139 | requestMap[requestId] = RequestContinuation(requestId, continuation, "connect") 140 | postBrokerLogEvent(LogLevel.INFO, "【行情接口登录】连接前置服务器...") 141 | mdApi.Init() 142 | inited = true 143 | } 144 | } 145 | 146 | /** 147 | * 关闭并释放资源,会发送一条 [BrokerEventType.CONNECTION] ([ConnectionEventType.MD_NET_DISCONNECTED]) 信息 148 | */ 149 | fun close() { 150 | if (frontConnected) mdSpi.OnFrontDisconnected(0) 151 | subscriptions.clear() 152 | codeMap.clear() 153 | mdApi.Release() 154 | mdApi.delete() 155 | } 156 | 157 | /** 158 | * 获取当前交易日 159 | */ 160 | fun getTradingDay(): String { 161 | return if (connected) tradingDay else mdApi.GetTradingDay() 162 | } 163 | 164 | /** 165 | * 查询当前已订阅的合约。[useCache] 及 [extras] 参数暂时无用 166 | */ 167 | fun querySubscriptions(useCache: Boolean, extras: Map?): List = subscriptions.toList() 168 | 169 | /** 170 | * 订阅行情。合约代码格式为 ExchangeID.InstrumentID。会自动检查合约订阅状态防止重复订阅。[extras.isForce: Boolean = false]【是否强制向交易所发送未更改的订阅请求(默认只发送未/已被订阅的标的的订阅请求)】 171 | */ 172 | suspend fun subscribeMarketData(codes: Collection, extras: Map? = null) { 173 | if (codes.isEmpty()) return 174 | val filteredCodes = if (extras?.get("isForce") != "true") codes.filter { it !in subscriptions } else codes 175 | if (filteredCodes.isEmpty()) return 176 | // CTP 行情订阅目前(2021.07)每34个订阅会丢失一个订阅(OnRspSubMarketData 中会每34个回调返回一个 bIsLast 为 true),所以需要分割 177 | if (filteredCodes.size >= 34) { 178 | val fullCodes = filteredCodes.toList() 179 | var startIndex = 0 180 | while (startIndex < filteredCodes.size) { 181 | subscribeMarketData(fullCodes.subList(startIndex, min(startIndex + 33, filteredCodes.size))) 182 | startIndex += 33 183 | } 184 | } else { // codes 长度小于34,直接订阅 185 | val rawCodes = filteredCodes.map { code -> 186 | val instrumentId = parseCode(code).second 187 | if (codeMap[instrumentId] == null) codeMap[instrumentId] = code 188 | instrumentId 189 | }.toTypedArray() 190 | val requestId = nextRequestId() 191 | runWithResultCheck({ mdApi.SubscribeMarketData(rawCodes) }, { 192 | suspendCoroutineWithTimeout(TIMEOUT_MILLS) { continuation -> 193 | // data 为订阅的 instrumentId 可变集合,在 CtpMdSpi.OnRspSubMarketData 中每收到一条合约订阅成功回报,就将该 instrumentId 从该可变集合中移除。当集合为空时,表明请求完成 194 | requestMap[requestId] = RequestContinuation(requestId, continuation, "subscribeMarketData", rawCodes.toMutableSet()) 195 | } 196 | }) 197 | } 198 | } 199 | 200 | /** 201 | * 退订行情。合约代码格式为 ExchangeID.InstrumentID。会自动检查合约订阅状态防止重复退订。[extras.isForce: Boolean = false]【是否强制向交易所发送未更改的订阅请求(默认只发送未/已被订阅的标的的订阅请求)】 202 | */ 203 | suspend fun unsubscribeMarketData(codes: Collection, extras: Map? = null) { 204 | if (codes.isEmpty()) return 205 | val filteredCodes = if (extras?.get("isForce") != "true") codes.filter { it in subscriptions } else codes 206 | if (filteredCodes.isEmpty()) return 207 | val rawCodes = filteredCodes.map { parseCode(it).second }.toTypedArray() 208 | val requestId = nextRequestId() 209 | runWithResultCheck({ mdApi.UnSubscribeMarketData(rawCodes) }, { 210 | suspendCoroutineWithTimeout(TIMEOUT_MILLS) { continuation -> 211 | requestMap[requestId] = RequestContinuation(requestId, continuation, "unsubscribeMarketData", rawCodes.toMutableSet()) 212 | } 213 | }) 214 | } 215 | 216 | /** 217 | * 订阅全市场合约行情。会自动检查合约订阅状态防止重复订阅。[extras.isForce: Boolean = false]【是否强制向交易所发送未更改的订阅请求(默认只发送未/已被订阅的标的的订阅请求)】 218 | */ 219 | suspend fun subscribeAllMarketData(extras: Map? = null) { 220 | val codes = tdApi.instruments.keys 221 | if (codes.isEmpty()) throw Exception("交易前置未连接,无法获得全市场合约") 222 | subscribeMarketData(codes, extras) 223 | } 224 | 225 | /** 226 | * 退订所有已订阅的合约行情。会自动检查合约订阅状态防止重复退订。[extras.isForce: Boolean = false]【是否强制向交易所发送未更改的订阅请求(默认只发送未/已被订阅的标的的订阅请求)】 227 | */ 228 | suspend fun unsubscribeAllMarketData(extras: Map? = null) { 229 | unsubscribeMarketData(subscriptions.toList(), extras) 230 | } 231 | 232 | /** 233 | * Ctp MdApi 的回调类 234 | */ 235 | private inner class CtpMdSpi : CThostFtdcMdSpi() { 236 | 237 | /** 238 | * 发生错误时回调。如果没有对应的协程请求,会发送一条 [BrokerEventType.LOG] 信息;有对应的协程请求时,会将其异常完成 239 | */ 240 | override fun OnRspError(pRspInfo: CThostFtdcRspInfoField, nRequestID: Int, bIsLast: Boolean) { 241 | val request = requestMap[nRequestID] 242 | if (request == null) { 243 | val errorInfo = "${pRspInfo.errorMsg}, requestId=$nRequestID, isLast=$bIsLast" 244 | val connectRequests = requestMap.values.filter { it.tag == "connect" } 245 | if (connectRequests.isEmpty()) { 246 | postBrokerLogEvent(LogLevel.ERROR, "【CtpMdSpi.OnRspError】$errorInfo") 247 | } else { 248 | resumeRequestsWithException("connect", errorInfo) 249 | } 250 | } else { 251 | request.continuation.resumeWithException(Exception(pRspInfo.errorMsg)) 252 | requestMap.remove(nRequestID) 253 | } 254 | } 255 | 256 | /** 257 | * 行情前置连接时回调。会将 [requestId] 置为 0;发送一条 [BrokerEventType.CONNECTION] 信息;自动请求用户登录 mdApi.ReqUserLogin(登录成功后 [connected] 才会置为 true),参见 [OnRspUserLogin] 258 | */ 259 | override fun OnFrontConnected() { 260 | frontConnected = true 261 | requestId.set(0) 262 | postBrokerConnectionEvent(ConnectionEventType.MD_NET_CONNECTED) 263 | runBlocking { 264 | runWithResultCheck({ mdApi.ReqUserLogin(CThostFtdcReqUserLoginField(), nextRequestId()) }, {}, { code, info -> 265 | resumeRequestsWithException("connect", "请求用户登录失败:$info, $code") 266 | }) 267 | } 268 | } 269 | 270 | /** 271 | * 行情前置断开连接时回调。会将 [connected] 置为 false;清空 [lastTicks];发送一条 [BrokerEventType.CONNECTION] 信息;异常完成所有的协程请求 272 | */ 273 | override fun OnFrontDisconnected(nReason: Int) { 274 | frontConnected = false 275 | connected = false 276 | lastTicks.clear() 277 | postBrokerConnectionEvent(ConnectionEventType.MD_NET_DISCONNECTED, "${getDisconnectReason(nReason)} ($nReason)") 278 | val e = Exception("网络连接断开:${getDisconnectReason(nReason)} ($nReason)") 279 | requestMap.values.forEach { 280 | it.continuation.resumeWithException(e) 281 | } 282 | requestMap.clear() 283 | } 284 | 285 | /** 286 | * 用户登录结果回调。登录成功后 [connected] 会置为 true。如果判断是发生了日内断网重连,会自动重新订阅断连前的已订阅合约。如果交易日变更,已订阅列表会清空。 287 | */ 288 | override fun OnRspUserLogin( 289 | pRspUserLogin: CThostFtdcRspUserLoginField?, 290 | pRspInfo: CThostFtdcRspInfoField?, 291 | nRequestID: Int, 292 | bIsLast: Boolean 293 | ) { 294 | checkRspInfo(pRspInfo, { 295 | if (pRspUserLogin == null) { 296 | resumeRequestsWithException("connect", "请求用户登录失败:pRspUserLogin 为 null") 297 | return 298 | } 299 | connected = true 300 | // 如果当日已订阅列表不为空,则说明发生了日内断网重连,自动重新订阅 301 | if (subscriptions.isNotEmpty() && tradingDay == pRspUserLogin.tradingDay) { 302 | GlobalScope.launch { 303 | runWithRetry({ subscribeMarketData(subscriptions.toList(), mapOf("isForce" to "true")) }, { e -> 304 | postBrokerLogEvent(LogLevel.ERROR, "【CtpMdSpi.OnRspUserLogin】重连后自动订阅行情失败:$e") 305 | }) 306 | } 307 | } 308 | // 如果交易日变更,则清空当日已订阅列表 309 | if (tradingDay != pRspUserLogin.tradingDay) { 310 | subscriptions.clear() 311 | tradingDay = pRspUserLogin.tradingDay 312 | } 313 | postBrokerConnectionEvent(ConnectionEventType.MD_LOGGED_IN) 314 | resumeRequests("connect", Unit) 315 | }, { errorCode, errorMsg -> 316 | resumeRequestsWithException("connect", "请求用户登录失败:$errorMsg ($errorCode)") 317 | }) 318 | } 319 | 320 | /** 321 | * 行情订阅结果回调。 322 | */ 323 | override fun OnRspSubMarketData( 324 | pSpecificInstrument: CThostFtdcSpecificInstrumentField?, 325 | pRspInfo: CThostFtdcRspInfoField?, 326 | nRequestID: Int, 327 | bIsLast: Boolean 328 | ) { 329 | if (pSpecificInstrument == null) { 330 | resumeRequestsWithException("subscribeMarketData", "请求订阅行情失败:pSpecificInstrument 为 null") 331 | return 332 | } 333 | val instrumentId = pSpecificInstrument.instrumentID 334 | val code = getCode(instrumentId) 335 | checkRspInfo(pRspInfo, { 336 | subscriptions.add(code) 337 | resumeRequests("subscribeMarketData", Unit) { req -> 338 | val subscribeSet = req.data as MutableSet 339 | subscribeSet.remove(instrumentId) 340 | subscribeSet.isEmpty() 341 | } 342 | }, { errorCode, errorMsg -> 343 | resumeRequestsWithException("subscribeMarketData", "请求订阅行情失败($code):$errorMsg ($errorCode)") { req -> 344 | (req.data as MutableSet).contains(instrumentId) 345 | } 346 | }) 347 | } 348 | 349 | /** 350 | * 行情退订结果回调。 351 | */ 352 | override fun OnRspUnSubMarketData( 353 | pSpecificInstrument: CThostFtdcSpecificInstrumentField?, 354 | pRspInfo: CThostFtdcRspInfoField?, 355 | nRequestID: Int, 356 | bIsLast: Boolean 357 | ) { 358 | if (pSpecificInstrument == null) { 359 | resumeRequestsWithException("unsubscribeMarketData", "请求退订行情失败:pSpecificInstrument 为 null") 360 | return 361 | } 362 | val instrumentId = pSpecificInstrument.instrumentID 363 | val code = getCode(instrumentId) 364 | checkRspInfo(pRspInfo, { 365 | subscriptions.remove(code) 366 | lastTicks.remove(code) 367 | resumeRequests("unsubscribeMarketData", Unit) { req -> 368 | val subscribeSet = req.data as MutableSet 369 | subscribeSet.remove(instrumentId) 370 | subscribeSet.isEmpty() 371 | } 372 | }, { errorCode, errorMsg -> 373 | resumeRequestsWithException("unsubscribeMarketData", "请求退订行情失败($code):$errorMsg ($errorCode)") { req -> 374 | (req.data as MutableSet).contains(instrumentId) 375 | } 376 | }) 377 | } 378 | 379 | /** 380 | * 行情推送回调。行情会以 [BrokerEventType.TICK] 信息发送 381 | */ 382 | override fun OnRtnDepthMarketData(data: CThostFtdcDepthMarketDataField) { 383 | val code = getCode(data.instrumentID) 384 | val lastTick = lastTicks[code] 385 | val newTick = Converter.tickC2A(code, data, lastTick, tdApi.instruments[code]?.volumeMultiple, tdApi.getInstrumentStatus(code)) { e -> 386 | postBrokerLogEvent(LogLevel.ERROR, "【CtpMdSpi.OnRtnDepthMarketData】Tick updateTime 解析失败:$code, ${data.updateTime}.${data.updateMillisec}, $e") 387 | } 388 | lastTicks[code] = newTick 389 | // 过滤掉订阅时自动推送的第一笔数据 390 | if (lastTick != null) postBrokerEvent(BrokerEventType.TICK, newTick) 391 | } 392 | } 393 | } -------------------------------------------------------------------------------- /lib/src/main/kotlin/org/rationalityfrontline/ktrader/broker/ctp/Converter.kt: -------------------------------------------------------------------------------- 1 | package org.rationalityfrontline.ktrader.broker.ctp 2 | 3 | import org.rationalityfrontline.jctp.* 4 | import org.rationalityfrontline.ktrader.datatype.* 5 | import java.time.LocalDate 6 | import java.time.LocalDateTime 7 | import java.time.LocalTime 8 | import java.time.format.DateTimeFormatter 9 | 10 | /** 11 | * 翻译器,用于将本地的 CTP 信息翻译为标准的 BrokerApi 信息 12 | */ 13 | @Suppress("MemberVisibilityCanBePrivate") 14 | internal object Converter { 15 | 16 | private val THOST_FTDC_OF_Open_S = jctpConstants.THOST_FTDC_OF_Open.toString() 17 | private val THOST_FTDC_OF_Close_S = jctpConstants.THOST_FTDC_OF_Close.toString() 18 | private val THOST_FTDC_OF_CloseToday_S = jctpConstants.THOST_FTDC_OF_CloseToday.toString() 19 | private val THOST_FTDC_OF_CloseYesterday_S = jctpConstants.THOST_FTDC_OF_CloseYesterday.toString() 20 | val THOST_FTDC_HF_Speculation = jctpConstants.THOST_FTDC_HF_Speculation.toString() 21 | 22 | fun dateC2A(date: String): LocalDate { 23 | return LocalDate.parse("${date.slice(0..3)}-${date.slice(4..5)}-${date.slice(6..7)}") 24 | } 25 | 26 | fun marginPriceTypeC2A(type: Char): MarginPriceType { 27 | return when (type) { 28 | jctpConstants.THOST_FTDC_MPT_PreSettlementPrice -> MarginPriceType.PRE_SETTLEMENT_PRICE 29 | jctpConstants.THOST_FTDC_MPT_SettlementPrice -> MarginPriceType.LAST_PRICE 30 | jctpConstants.THOST_FTDC_MPT_AveragePrice -> MarginPriceType.TODAY_SETTLEMENT_PRICE 31 | jctpConstants.THOST_FTDC_MPT_OpenPrice -> MarginPriceType.OPEN_PRICE 32 | jctpConstants.THOST_FTDC_ORPT_MaxPreSettlementPrice -> MarginPriceType.MAX_PRE_SETTLEMENT_PRICE_LAST_PRICE 33 | else -> MarginPriceType.PRE_SETTLEMENT_PRICE 34 | } 35 | } 36 | 37 | fun directionA2C(direction: Direction): Char { 38 | return when (direction) { 39 | Direction.LONG -> jctpConstants.THOST_FTDC_D_Buy 40 | Direction.SHORT -> jctpConstants.THOST_FTDC_D_Sell 41 | Direction.UNKNOWN -> throw IllegalArgumentException("不允许输入 UNKNOWN") 42 | } 43 | } 44 | 45 | fun directionC2A(direction: Char): Direction { 46 | return when (direction) { 47 | jctpConstants.THOST_FTDC_D_Buy -> Direction.LONG 48 | jctpConstants.THOST_FTDC_D_Sell -> Direction.SHORT 49 | jctpConstants.THOST_FTDC_PD_Long -> Direction.LONG 50 | jctpConstants.THOST_FTDC_PD_Short -> Direction.SHORT 51 | else -> Direction.UNKNOWN 52 | } 53 | } 54 | 55 | fun offsetA2C(offset: OrderOffset): String { 56 | return when (offset) { 57 | OrderOffset.OPEN -> THOST_FTDC_OF_Open_S 58 | OrderOffset.CLOSE -> THOST_FTDC_OF_Close_S 59 | OrderOffset.CLOSE_TODAY -> THOST_FTDC_OF_CloseToday_S 60 | OrderOffset.CLOSE_YESTERDAY -> THOST_FTDC_OF_CloseYesterday_S 61 | OrderOffset.UNKNOWN -> throw IllegalArgumentException("不允许输入 UNKNOWN") 62 | } 63 | } 64 | 65 | fun offsetC2A(offset: String): OrderOffset { 66 | return when (offset) { 67 | THOST_FTDC_OF_Open_S -> OrderOffset.OPEN 68 | THOST_FTDC_OF_Close_S -> OrderOffset.CLOSE 69 | THOST_FTDC_OF_CloseToday_S -> OrderOffset.CLOSE_TODAY 70 | THOST_FTDC_OF_CloseYesterday_S -> OrderOffset.CLOSE_YESTERDAY 71 | else -> OrderOffset.UNKNOWN 72 | } 73 | } 74 | 75 | fun tickC2A(code: String, tickField: CThostFtdcDepthMarketDataField, lastTick: Tick? = null, volumeMultiple: Int? = null, marketStatus: MarketStatus = MarketStatus.UNKNOWN, onTimeParseError: (Exception) -> Unit): Tick { 76 | val updateTime = try { 77 | LocalTime.parse("${tickField.updateTime}.${tickField.updateMillisec}").atDate(LocalDate.now()) 78 | } catch (e: Exception) { 79 | onTimeParseError(e) 80 | LocalDateTime.now() 81 | } 82 | val lastPrice = formatDouble(tickField.lastPrice) 83 | val bidPrice = arrayOf(formatDouble(tickField.bidPrice1), formatDouble(tickField.bidPrice2), formatDouble(tickField.bidPrice3), formatDouble(tickField.bidPrice4), formatDouble(tickField.bidPrice5)) 84 | val askPrice = arrayOf(formatDouble(tickField.askPrice1), formatDouble(tickField.askPrice2), formatDouble(tickField.askPrice3), formatDouble(tickField.askPrice4), formatDouble(tickField.askPrice5)) 85 | return Tick( 86 | code = code, 87 | time = updateTime, 88 | lastPrice = lastPrice, 89 | bidPrice = bidPrice, 90 | askPrice = askPrice, 91 | bidVolume = arrayOf(tickField.bidVolume1, tickField.bidVolume2, tickField.bidVolume3, tickField.bidVolume4, tickField.bidVolume5), 92 | askVolume = arrayOf(tickField.askVolume1, tickField.askVolume2, tickField.askVolume3, tickField.askVolume4, tickField.askVolume5), 93 | volume = tickField.volume - (lastTick?.todayVolume ?: tickField.volume), 94 | turnover = tickField.turnover - (lastTick?.todayTurnover ?: tickField.turnover), 95 | openInterestDelta = tickField.openInterest.toInt() - (lastTick?.todayOpenInterest ?: tickField.openInterest.toInt()), 96 | direction = if (lastTick == null) calculateTickDirection(lastPrice, bidPrice[0], askPrice[0]) else calculateTickDirection(lastPrice, lastTick.bidPrice[0], lastTick.askPrice[0]), 97 | status = marketStatus, 98 | preClosePrice = formatDouble(tickField.preClosePrice), 99 | preSettlementPrice = formatDouble(tickField.preSettlementPrice), 100 | preOpenInterest = tickField.preOpenInterest.toInt(), 101 | todayOpenPrice = formatDouble(tickField.openPrice), 102 | todayClosePrice = formatDouble(tickField.closePrice), 103 | todayHighPrice = formatDouble(tickField.highestPrice), 104 | todayLowPrice = formatDouble(tickField.lowestPrice), 105 | todayHighLimitPrice = formatDouble(tickField.upperLimitPrice), 106 | todayLowLimitPrice = formatDouble(tickField.lowerLimitPrice), 107 | todayAvgPrice = if (volumeMultiple == null || volumeMultiple == 0 || tickField.volume == 0) 0.0 else tickField.turnover / (volumeMultiple * tickField.volume), 108 | todayVolume = tickField.volume, 109 | todayTurnover = formatDouble(tickField.turnover), 110 | todaySettlementPrice = formatDouble(tickField.settlementPrice), 111 | todayOpenInterest = tickField.openInterest.toInt(), 112 | ) 113 | } 114 | 115 | /** 116 | * 行情推送的 [Tick] 中很多字段可能是无效值,CTP 内用 [Double.MAX_VALUE] 表示,在此需要统一为 0.0 117 | */ 118 | @Suppress("NOTHING_TO_INLINE") 119 | private inline fun formatDouble(input: Double): Double { 120 | return if (input == Double.MAX_VALUE) 0.0 else input 121 | } 122 | 123 | /** 124 | * 计算 Tick 方向 125 | */ 126 | private fun calculateTickDirection(lastPrice: Double, bid1Price: Double, ask1Price: Double): TickDirection { 127 | return when { 128 | lastPrice >= ask1Price -> TickDirection.UP 129 | lastPrice <= bid1Price -> TickDirection.DOWN 130 | else -> TickDirection.STAY 131 | } 132 | } 133 | 134 | fun securityC2A(insField: CThostFtdcInstrumentField, onTimeParseError: (Exception) -> Unit): SecurityInfo? { 135 | return try { 136 | val type = when (insField.productClass) { 137 | jctpConstants.THOST_FTDC_PC_Futures -> SecurityType.FUTURES 138 | jctpConstants.THOST_FTDC_PC_Options, 139 | jctpConstants.THOST_FTDC_PC_SpotOption, -> SecurityType.OPTIONS 140 | else -> SecurityType.UNKNOWN 141 | } 142 | if (type == SecurityType.UNKNOWN) null else { 143 | SecurityInfo( 144 | code = "${insField.exchangeID}.${insField.instrumentID}", 145 | type = type, 146 | productId = insField.productID, 147 | name = insField.instrumentName, 148 | priceTick = insField.priceTick, 149 | volumeMultiple = insField.volumeMultiple, 150 | isTrading = insField.isTrading != 0, 151 | openDate = LocalDate.parse(insField.openDate, DateTimeFormatter.BASIC_ISO_DATE), 152 | expireDate = LocalDate.parse(insField.expireDate, DateTimeFormatter.BASIC_ISO_DATE), 153 | endDeliveryDate = LocalDate.parse(insField.endDelivDate, DateTimeFormatter.BASIC_ISO_DATE), 154 | isUseMaxMarginSideAlgorithm = insField.maxMarginSideAlgorithm == jctpConstants.THOST_FTDC_MMSA_YES, 155 | optionsType = when (insField.optionsType) { 156 | jctpConstants.THOST_FTDC_CP_CallOptions -> OptionsType.CALL 157 | jctpConstants.THOST_FTDC_CP_PutOptions -> OptionsType.PUT 158 | else -> OptionsType.UNKNOWN 159 | }, 160 | optionsUnderlyingCode = "${insField.exchangeID}.${insField.underlyingInstrID}", 161 | optionsStrikePrice = formatDouble(insField.strikePrice) 162 | ) 163 | } 164 | } catch (e: Exception) { 165 | onTimeParseError(e) 166 | null 167 | } 168 | } 169 | 170 | fun orderC2A(orderField: CThostFtdcOrderField, volumeMultiple: Int, onTimeParseError: (Exception) -> Unit): Order { 171 | val orderId = "${orderField.frontID}_${orderField.sessionID}_${orderField.orderRef}" 172 | val orderType = when (orderField.orderPriceType) { 173 | jctpConstants.THOST_FTDC_OPT_LimitPrice -> when (orderField.timeCondition) { 174 | jctpConstants.THOST_FTDC_TC_GFD -> OrderType.LIMIT 175 | jctpConstants.THOST_FTDC_TC_IOC -> when (orderField.volumeCondition) { 176 | jctpConstants.THOST_FTDC_VC_CV -> OrderType.FOK 177 | else -> OrderType.FAK 178 | } 179 | else -> OrderType.UNKNOWN 180 | } 181 | jctpConstants.THOST_FTDC_OPT_AnyPrice -> OrderType.MARKET 182 | else -> OrderType.UNKNOWN 183 | } 184 | val orderStatus = when (orderField.orderSubmitStatus) { 185 | jctpConstants.THOST_FTDC_OSS_InsertRejected -> OrderStatus.ERROR 186 | else -> when (orderField.orderStatus) { 187 | jctpConstants.THOST_FTDC_OST_Unknown -> OrderStatus.SUBMITTING 188 | jctpConstants.THOST_FTDC_OST_NoTradeQueueing -> OrderStatus.ACCEPTED 189 | jctpConstants.THOST_FTDC_OST_PartTradedQueueing -> OrderStatus.PARTIALLY_FILLED 190 | jctpConstants.THOST_FTDC_OST_AllTraded -> OrderStatus.FILLED 191 | jctpConstants.THOST_FTDC_OST_Canceled -> OrderStatus.CANCELED 192 | else -> OrderStatus.UNKNOWN 193 | } 194 | } 195 | val createTime = try { 196 | val date = orderField.insertDate 197 | LocalDateTime.parse("${date.slice(0..3)}-${date.slice(4..5)}-${date.slice(6..7)}T${orderField.insertTime}") 198 | } catch (e: Exception) { 199 | onTimeParseError(e) 200 | LocalDateTime.now() 201 | } 202 | val updateTime = if (orderStatus == OrderStatus.CANCELED) { 203 | try { 204 | LocalTime.parse(orderField.cancelTime).atDate(LocalDate.now()) 205 | } catch (e: Exception) { 206 | onTimeParseError(e) 207 | LocalDateTime.now() 208 | } 209 | } else createTime 210 | return Order( 211 | orderField.investorID, 212 | orderId, "${orderField.exchangeID}.${orderField.instrumentID}", 213 | orderField.limitPrice, null, orderField.volumeTotalOriginal, orderField.minVolume, directionC2A(orderField.direction), 214 | offsetC2A(orderField.combOffsetFlag), orderType, orderStatus, orderField.statusMsg, 215 | orderField.volumeTraded, orderField.limitPrice * orderField.volumeTraded * volumeMultiple, orderField.limitPrice, 0.0, 0.0, 216 | createTime, updateTime, extras = mutableMapOf() 217 | ).apply { 218 | if (orderField.orderSysID.isNotEmpty()) orderSysId = "${orderField.exchangeID}_${orderField.orderSysID}" 219 | } 220 | } 221 | 222 | fun tradeC2A(tradeField: CThostFtdcTradeField, orderId: String, onTimeParseError: (Exception) -> Unit): Trade { 223 | val tradeTime = try { 224 | val date = tradeField.tradeDate 225 | val updateTimeStr = "${date.slice(0..3)}-${date.slice(4..5)}-${date.slice(6..7)}T${tradeField.tradeTime}" 226 | LocalDateTime.parse(updateTimeStr) 227 | } catch (e: Exception) { 228 | onTimeParseError(e) 229 | LocalDateTime.now() 230 | } 231 | return Trade( 232 | accountId = tradeField.investorID, 233 | tradeId = "${tradeField.tradeID}_${tradeField.orderRef}", 234 | orderId = orderId, 235 | code = "${tradeField.exchangeID}.${tradeField.instrumentID}", 236 | price = tradeField.price, 237 | volume = tradeField.volume, 238 | turnover = 0.0, 239 | direction = directionC2A(tradeField.direction), 240 | offset = offsetC2A(tradeField.offsetFlag.toString()), 241 | commission = 0.0, 242 | time = tradeTime 243 | ) 244 | } 245 | 246 | fun positionC2A(tradingDay: LocalDate, positionField: CThostFtdcInvestorPositionField): Position { 247 | val direction = directionC2A(positionField.posiDirection) 248 | val frozenVolume = when (direction) { 249 | Direction.LONG -> positionField.shortFrozen 250 | Direction.SHORT -> positionField.longFrozen 251 | else -> 0 252 | } 253 | return Position( 254 | accountId = positionField.investorID, 255 | tradingDay = tradingDay, 256 | code = "${positionField.exchangeID}.${positionField.instrumentID}", 257 | direction = direction, 258 | preVolume = positionField.ydPosition, 259 | volume = positionField.position, 260 | value = positionField.useMargin, 261 | todayVolume = positionField.todayPosition, 262 | frozenVolume = frozenVolume, 263 | frozenTodayVolume = 0, 264 | todayOpenVolume = positionField.openVolume, 265 | todayCloseVolume = positionField.closeVolume, 266 | todayCommission = positionField.commission, 267 | openCost = positionField.openCost, 268 | avgOpenPrice = 0.0, 269 | lastPrice = 0.0, 270 | pnl = 0.0, 271 | ) 272 | } 273 | 274 | fun assetsC2A(tradingDay: LocalDate, assetsField: CThostFtdcTradingAccountField): Assets { 275 | return Assets( 276 | accountId = assetsField.accountID, 277 | tradingDay = tradingDay, 278 | total = assetsField.balance, 279 | available = assetsField.available, 280 | positionValue = assetsField.currMargin, 281 | frozenByOrder = assetsField.frozenCash, 282 | todayCommission = assetsField.commission, 283 | initialCash = 0.0, 284 | totalClosePnl = 0.0, 285 | totalCommission = 0.0, 286 | positionPnl = 0.0, 287 | ) 288 | } 289 | 290 | fun futuresCommissionRateC2A(crField: CThostFtdcInstrumentCommissionRateField, code: String): CommissionRate { 291 | return CommissionRate( 292 | code = code, 293 | openRatioByMoney = crField.openRatioByMoney, 294 | openRatioByVolume = crField.openRatioByVolume, 295 | closeRatioByMoney = crField.closeRatioByMoney, 296 | closeRatioByVolume = crField.closeRatioByVolume, 297 | closeTodayRatioByMoney = crField.closeTodayRatioByMoney, 298 | closeTodayRatioByVolume = crField.closeTodayRatioByVolume, 299 | ) 300 | } 301 | 302 | fun optionsCommissionRateC2A(crField: CThostFtdcOptionInstrCommRateField): CommissionRate { 303 | return CommissionRate( 304 | code = crField.instrumentID, 305 | openRatioByMoney = crField.openRatioByMoney, 306 | openRatioByVolume = crField.openRatioByVolume, 307 | closeRatioByMoney = crField.closeRatioByMoney, 308 | closeRatioByVolume = crField.closeRatioByVolume, 309 | closeTodayRatioByMoney = crField.closeTodayRatioByMoney, 310 | closeTodayRatioByVolume = crField.closeTodayRatioByVolume, 311 | optionsStrikeRatioByMoney = crField.strikeRatioByMoney, 312 | optionsStrikeRatioByVolume = crField.strikeRatioByVolume, 313 | ) 314 | } 315 | 316 | fun futuresMarginRateC2A(mrField: CThostFtdcInstrumentMarginRateField, code: String): MarginRate { 317 | return MarginRate( 318 | code = code, 319 | longMarginRatioByMoney = mrField.longMarginRatioByMoney, 320 | longMarginRatioByVolume = mrField.longMarginRatioByVolume, 321 | shortMarginRatioByMoney = mrField.shortMarginRatioByMoney, 322 | shortMarginRatioByVolume = mrField.shortMarginRatioByVolume, 323 | ) 324 | } 325 | 326 | fun optionsMarginC2A(mrField: CThostFtdcOptionInstrTradeCostField, code: String): MarginRate { 327 | return MarginRate( 328 | code = code, 329 | longMarginRatioByMoney = mrField.fixedMargin, 330 | longMarginRatioByVolume = mrField.exchFixedMargin, 331 | shortMarginRatioByMoney = mrField.miniMargin, 332 | shortMarginRatioByVolume = mrField.exchMiniMargin, 333 | ) 334 | } 335 | } --------------------------------------------------------------------------------