diff --git a/messages.pot b/messages.pot index 450c1ec12..cf814f535 100644 --- a/messages.pot +++ b/messages.pot @@ -1,21 +1,21 @@ # Translations template for PROJECT. -# Copyright (C) 2022 ORGANIZATION +# Copyright (C) 2024 ORGANIZATION # This file is distributed under the same license as the PROJECT project. -# FIRST AUTHOR , 2022. +# FIRST AUTHOR , 2024. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2022-06-24 10:33+0800\n" +"POT-Creation-Date: 2024-01-19 17:56+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.10.1\n" +"Generated-By: Babel 2.13.1\n" #: rqalpha/environment.py:70 msgid "" @@ -27,13 +27,13 @@ msgstr "" msgid "No such transaction cost decider, order_book_id = {}" msgstr "" -#: rqalpha/main.py:61 +#: rqalpha/main.py:60 msgid "" "There is no data between {start_date} and {end_date}. Please check your " "data bundle or select other backtest period." msgstr "" -#: rqalpha/main.py:80 +#: rqalpha/main.py:79 msgid "" "Missing persist provider. You need to set persist_provider before use " "persist" @@ -43,123 +43,119 @@ msgstr "" msgid "rqdatac init failed, some apis will not function properly: {}" msgstr "" -#: rqalpha/main.py:213 +#: rqalpha/main.py:219 msgid "system restored" msgstr "" -#: rqalpha/main.py:243 +#: rqalpha/main.py:249 msgid "strategy run successfully, normal exit" msgstr "" -#: rqalpha/main.py:248 +#: rqalpha/main.py:254 msgid "strategy execute exception" msgstr "" -#: rqalpha/apis/api_base.py:67 rqalpha/apis/api_base.py:266 -#: rqalpha/apis/api_base.py:302 +#: rqalpha/apis/api_base.py:69 rqalpha/apis/api_base.py:286 +#: rqalpha/apis/api_base.py:322 msgid "unsupported order_book_id type" msgstr "" -#: rqalpha/apis/api_base.py:87 rqalpha/apis/api_base.py:91 -msgid "Limit order price should not be nan." -msgstr "" - -#: rqalpha/apis/api_base.py:148 +#: rqalpha/apis/api_base.py:164 #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:59 msgid "Main Future contracts[88] are not supported in paper trading." msgstr "" -#: rqalpha/apis/api_base.py:152 +#: rqalpha/apis/api_base.py:168 #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:61 msgid "Index Future contracts[99] are not supported in paper trading." msgstr "" -#: rqalpha/apis/api_base.py:158 +#: rqalpha/apis/api_base.py:174 #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:66 -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:89 -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:128 -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:308 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:104 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:142 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:346 msgid "Order Creation Failed: [{order_book_id}] No market data" msgstr "" -#: rqalpha/apis/api_rqdatac.py:46 +#: rqalpha/apis/api_rqdatac.py:50 msgid "rqdatac is not available, extension apis will not function properly" msgstr "" -#: rqalpha/apis/api_rqdatac.py:102 +#: rqalpha/apis/api_rqdatac.py:106 msgid "in get_split, start_date {} is no earlier than the previous test day {}" msgstr "" -#: rqalpha/apis/api_rqdatac.py:140 +#: rqalpha/apis/api_rqdatac.py:144 msgid "in index_components, date {} is no earlier than test date {}" msgstr "" -#: rqalpha/apis/api_rqdatac.py:187 +#: rqalpha/apis/api_rqdatac.py:191 msgid "in index_weights, date {} is no earlier than previous test date {}" msgstr "" -#: rqalpha/apis/api_rqdatac.py:330 +#: rqalpha/apis/api_rqdatac.py:398 msgid "in get_price, end_date {} is no earlier than the previous test day {}" msgstr "" -#: rqalpha/apis/api_rqdatac.py:338 +#: rqalpha/apis/api_rqdatac.py:406 msgid "in get_price, start_date {} is no earlier than the previous test day {}" msgstr "" -#: rqalpha/apis/api_rqdatac.py:343 +#: rqalpha/apis/api_rqdatac.py:411 msgid "in get_price, start_date {} > end_date {}" msgstr "" -#: rqalpha/apis/api_rqdatac.py:805 +#: rqalpha/apis/api_rqdatac.py:873 msgid "'{0}' future does not exist" msgstr "" -#: rqalpha/apis/api_rqdatac.py:962 +#: rqalpha/apis/api_rqdatac.py:1159 msgid "in get_fundamentals entry_date {} is no earlier than test date {}" msgstr "" -#: rqalpha/apis/api_rqdatac.py:996 rqalpha/apis/api_rqdatac.py:1061 -#: rqalpha/utils/arg_checker.py:311 +#: rqalpha/apis/api_rqdatac.py:1193 rqalpha/apis/api_rqdatac.py:1258 +#: rqalpha/utils/arg_checker.py:320 msgid "" "function {}: invalid {} argument, quarter should be in form of '2012q3', " "got {} (type: {})" msgstr "" -#: rqalpha/cmds/bundle.py:33 +#: rqalpha/cmds/bundle.py:34 msgid "create bundle using RQDatac" msgstr "" -#: rqalpha/cmds/bundle.py:43 +#: rqalpha/cmds/bundle.py:44 msgid "" "rqdatac is required to create bundle. you can visit " "https://www.ricequant.com/welcome/rqdata to get rqdatac, or use \"rqalpha" " download-bundle\" to download monthly updated bundle." msgstr "" -#: rqalpha/cmds/bundle.py:54 rqalpha/cmds/bundle.py:83 +#: rqalpha/cmds/bundle.py:55 rqalpha/cmds/bundle.py:84 msgid "rqdatac init failed with error: {}" msgstr "" -#: rqalpha/cmds/bundle.py:62 +#: rqalpha/cmds/bundle.py:63 msgid "Update bundle using RQDatac" msgstr "" -#: rqalpha/cmds/bundle.py:72 +#: rqalpha/cmds/bundle.py:73 msgid "" "rqdatac is required to update bundle. you can visit " "https://www.ricequant.com/welcome/rqdata to get rqdatac, or use \"rqalpha" " download-bundle\" to download monthly updated bundle." msgstr "" -#: rqalpha/cmds/bundle.py:87 +#: rqalpha/cmds/bundle.py:88 msgid "bundle not exist, use \"rqalpha create-bundle\" command instead" msgstr "" -#: rqalpha/cmds/bundle.py:94 +#: rqalpha/cmds/bundle.py:95 msgid "Download bundle (monthly updated)" msgstr "" -#: rqalpha/cmds/bundle.py:105 +#: rqalpha/cmds/bundle.py:106 msgid "" "\n" " [WARNING]\n" @@ -168,24 +164,56 @@ msgid "" " Are you sure to continue?" msgstr "" -#: rqalpha/cmds/bundle.py:123 +#: rqalpha/cmds/bundle.py:124 msgid "Data bundle download successfully in {bundle_path}" msgstr "" -#: rqalpha/cmds/bundle.py:134 +#: rqalpha/cmds/bundle.py:127 +msgid "Check bundle" +msgstr "" + +#: rqalpha/cmds/bundle.py:141 msgid "try {} ..." msgstr "" -#: rqalpha/cmds/bundle.py:146 +#: rqalpha/cmds/bundle.py:153 msgid "downloading ..." msgstr "" -#: rqalpha/cmds/bundle.py:160 +#: rqalpha/cmds/bundle.py:167 msgid "" "\n" "Download failed, retry in {} seconds." msgstr "" +#: rqalpha/cmds/bundle.py:188 +msgid "corrupted files" +msgstr "" + +#: rqalpha/cmds/bundle.py:189 +msgid "remove files" +msgstr "" + +#: rqalpha/cmds/bundle.py:192 +msgid "remove success" +msgstr "" + +#: rqalpha/cmds/bundle.py:194 +msgid "corrupted files not remove" +msgstr "" + +#: rqalpha/cmds/bundle.py:196 +msgid "input error" +msgstr "" + +#: rqalpha/cmds/bundle.py:198 +msgid "bundle's day bar is incomplete, please update bundle" +msgstr "" + +#: rqalpha/cmds/bundle.py:200 +msgid "good bundle's day bar" +msgstr "" + #: rqalpha/cmds/misc.py:26 msgid "Generate example strategies to target folder" msgstr "" @@ -219,12 +247,50 @@ msgstr "" msgid "deprecated parameter[bar_dict] in before_trading function." msgstr "" -#: rqalpha/data/base_data_source/storages.py:85 +#: rqalpha/data/bundle.py:454 +msgid "" +"RQAlpha already supports backtesting using futures historical margins and" +" rates, please upgrade RQDatac to version 2.11.12 and above to use it" +msgstr "" + +#: rqalpha/data/bundle.py:464 rqalpha/data/bundle.py:522 +msgid "" +"Futures historical trading parameters data is being updated, please " +"wait......" +msgstr "" + +#: rqalpha/data/bundle.py:516 +msgid "" +"File {} update failed, if it is using, please update later, or you can " +"delete then update again" +msgstr "" + +#: rqalpha/data/base_data_source/data_source.py:140 +msgid "" +"RQDatac is not installed, " +"\"config.base.futures_time_series_trading_parameters\" will be disabled." +msgstr "" + +#: rqalpha/data/base_data_source/data_source.py:145 +msgid "" +"RQDatac does not have permission to obtain futures histrical trading " +"parameters, \"config.base.futures_time_series_trading_parameters\" will " +"be disabled." +msgstr "" + +#: rqalpha/data/base_data_source/storages.py:81 +msgid "" +"Your bundle data is too old, please use 'rqalpha update-bundle' or " +"'rqalpha download-bundle' to update it to lastest before using" +msgstr "" + +#: rqalpha/data/base_data_source/storages.py:97 +#: rqalpha/data/base_data_source/storages.py:123 msgid "unsupported future instrument {}" msgstr "" -#: rqalpha/data/base_data_source/storages.py:155 -#: rqalpha/data/base_data_source/storages.py:165 +#: rqalpha/data/base_data_source/storages.py:194 +#: rqalpha/data/base_data_source/storages.py:204 msgid "" "open data bundle failed, you can remove {} and try to regenerate bundle: " "{}" @@ -252,7 +318,7 @@ msgstr "" msgid "mod tear_down [END] {}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/position_model.py:296 +#: rqalpha/mod/rqalpha_mod_sys_accounts/position_model.py:314 msgid "{order_book_id} is expired, close all positions by system" msgstr "" @@ -270,15 +336,15 @@ msgid "" "{closable}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:49 -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:96 -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:144 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:50 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:112 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:158 msgid "Order Creation Failed: 0 order quantity, order_book_id={order_book_id}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:54 -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:85 -msgid "Limit order price should be positive" +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:55 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:100 +msgid "Limit order price should not be nan." msgstr "" #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:78 @@ -299,266 +365,337 @@ msgid "" "[{new_orders_repr}]" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:65 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:66 msgid "" "order_book_id: {order_book_id} needs stock account, please set and try " "again!" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:106 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:120 msgid "insufficient cash, use all remaining cash({}) to create order" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:296 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:334 msgid "" "function order_target_portfolio: invalid keys of target_portfolio, " "expected order_book_ids or Instrument objects, got {} (type: {})" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:301 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:339 msgid "" "function order_target_portfolio: invalid instrument type, excepted " "CS/ETF/LOF/INDX, got {}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:318 -msgid "" -"function order_target_portfolio: invalid order price {target_price} of " -"{id_or_ins}" -msgstr "" - -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:324 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:354 msgid "" "function order_target_portfolio: invalid values of target_portfolio, " "excepted float between 0 and 1, got {} (key: {})" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:332 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:363 msgid "total percent should be lower than 1, current: {}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:655 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:383 +msgid "" +"Adjust position of {id_or_ins} Failed: Invalid close/open price " +"{close_price}/{open_price}" +msgstr "" + +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:698 msgid "in get_dividend, start_date {} is later than the previous test day {}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:64 +#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:66 msgid "[sys_analyser] save report" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:69 +#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:71 msgid "[sys_analyser] output result pickle file" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:74 +#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:76 msgid "[sys_analyser] plot result" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:79 +#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:81 msgid "[sys_analyser] save plot to file" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:84 +#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:86 msgid "[sys_analyser] order_book_id of benchmark" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:89 +#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:91 msgid "[sys_analyser] show open close points on plot" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:94 +#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:96 msgid "[sys_analyser] show weekly indicators and return curve on plot" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:98 +#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:100 msgid "[sys_analyser] Plot from strategy output file" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:101 +#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:103 msgid "save plot result to file" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:102 +#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:104 msgid "show open close points on plot" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:103 +#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:105 msgid "show weekly indicators and return curve on plot" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:113 +#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:115 msgid "[sys_analyser] Generate report from strategy output file" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:108 +#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:112 msgid "" "config 'base.benchmark' is deprecated, use 'mod.sys_analyser.benchmark' " "instead" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:190 +#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:143 +msgid "benchmark {} not exists, please entry correct order_book_id" +msgstr "" + +#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:147 +msgid "" +"benchmark {} listed date {} > backtest start date {} or de_listed date {}" +" <= backtest end date {}" +msgstr "" + +#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:210 msgid "invalid init benchmark {}, should be in format 'order_book_id:weight'" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:195 +#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:215 msgid "invalid weight for instrument {order_book_id}: {weight}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:59 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:60 msgid "Strategy" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:60 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:61 msgid "Benchmark" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:61 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:62 msgid "Excess" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:62 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:63 msgid "Weekly" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:63 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:64 msgid "BenchmarkWeekly" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:65 -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:82 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:66 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:123 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:170 msgid "MaxDrawDown" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:66 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:67 msgid "MaxDDD" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:67 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:68 msgid "Open" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:68 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:69 msgid "Close" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:71 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:103 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:155 msgid "TotalReturns" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:72 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:104 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:156 msgid "AnnualReturns" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:73 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:105 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:157 msgid "Alpha" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:74 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:106 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:158 msgid "Beta" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:75 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:107 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:161 msgid "Sharpe" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:76 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:108 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:165 msgid "Sortino" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:77 -msgid "InformationRatio" +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:109 +msgid "WeeklyUlcerIndex" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:79 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:111 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:175 msgid "BenchmarkReturns" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:80 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:112 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:176 msgid "BenchmarkAnnual" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:81 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:113 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:167 msgid "Volatility" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:83 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:114 msgid "TrackingError" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:84 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:115 msgid "DownsideRisk" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:85 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:116 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:164 +msgid "InformationRatio" +msgstr "" + +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:117 +msgid "WeeklyUlcerPerformanceIndex" +msgstr "" + +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:119 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:179 +msgid "ExcessCumReturns" +msgstr "" + +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:120 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:159 +msgid "WinRate" +msgstr "" + +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:121 +msgid "WeeklyWinRate" +msgstr "" + +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:122 +msgid "ProfitLossRate" +msgstr "" + +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:124 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:171 msgid "MaxDD/MaxDDD" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:89 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:125 +msgid "WeeklyExcessUlcerIndex" +msgstr "" + +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:129 msgid "WeeklyAlpha" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:90 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:130 msgid "WeeklyBeta" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:91 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:131 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:162 msgid "WeeklySharpe" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:92 -msgid "WeeklySortino" -msgstr "" - -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:93 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:132 msgid "WeeklyInfoRatio" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:94 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:133 msgid "WeeklyTrackingError" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:95 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:134 msgid "WeeklyMaxDrawdown" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:99 -msgid "ExcessCumReturns" -msgstr "" - -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:100 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:138 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:177 msgid "ExcessReturns" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:101 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:139 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:178 msgid "ExcessAnnual" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:102 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:140 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:181 msgid "ExcessSharpe" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:103 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:141 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:182 msgid "ExcessVolatility" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:105 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:142 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:184 msgid "ExcessMaxDD" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:106 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:143 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:185 msgid "ExcessMaxDD/ExcessMaxDDD" msgstr "" +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:144 +msgid "WeeklyExcessUlcerPerformanceIndex" +msgstr "" + +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:163 +msgid "MonthlySharpe" +msgstr "" + +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:168 +msgid "WeeklyVolatility" +msgstr "" + +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:169 +msgid "MonthlyVolatility" +msgstr "" + +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:183 +msgid "ExcessWinRate" +msgstr "" + #: rqalpha/mod/rqalpha_mod_sys_risk/validators/cash_validator.py:33 msgid "" "Order Creation Failed: not enough money to buy {order_book_id}, needs " "{cost_money:.2f}, cash {cash:.2f}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_risk/validators/is_trading_validator.py:31 +#: rqalpha/mod/rqalpha_mod_sys_risk/validators/is_trading_validator.py:32 msgid "Order Creation Failed: {order_book_id} is not listing!" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_risk/validators/is_trading_validator.py:37 +#: rqalpha/mod/rqalpha_mod_sys_risk/validators/is_trading_validator.py:38 msgid "Order Creation Failed: security {order_book_id} is suspended on {date}" msgstr "" @@ -580,74 +717,72 @@ msgid "" "trade: [{}...]" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:101 -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:319 -#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:66 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:118 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:335 +#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:69 msgid "" "Order Cancelled: current security [{order_book_id}] can not be traded in " "listed date [{listed_date}]" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:108 -#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:72 -msgid "Order Cancelled: current bar [{order_book_id}] miss market data." -msgstr "" - -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:128 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:125 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:165 #: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:86 -msgid "Order Cancelled: current bar [{order_book_id}] reach the limit_up price." +msgid "Order Cancelled: {order_book_id} bar no volume" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:134 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:150 #: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:95 -msgid "Order Cancelled: current bar [{order_book_id}] reach the limit_down price." +msgid "Order Cancelled: current bar [{order_book_id}] reach the limit_up price." msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:143 -msgid "Order Cancelled: {order_book_id} bar no volume" +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:156 +#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:104 +msgid "Order Cancelled: current bar [{order_book_id}] reach the limit_down price." msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:159 -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:396 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:178 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:417 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:567 msgid "" "Order Cancelled: market order {order_book_id} volume {order_volume} due " "to volume limit" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:197 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:217 msgid "" "Order Cancelled: market order {order_book_id} volume {order_volume} is " "larger than {volume_percent_limit} percent of current bar volume, fill " "{filled_volume} actually" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:326 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:343 msgid "Order Cancelled: current tick [{order_book_id}] miss market data." msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:351 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:368 msgid "Order Cancelled: current tick [{order_book_id}] reach the limit_up price." msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:357 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:374 msgid "" "Order Cancelled: current tick [{order_book_id}] reach the limit_down " "price." msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:364 -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:370 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:381 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:387 msgid "Order Cancelled: [{order_book_id}] has no liquidity." msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:438 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:462 msgid "" "Order Cancelled: market order {order_book_id} volume {order_volume} is " "larger than {volume_percent_limit} percent of current tick volume, fill " "{filled_volume} actually" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:571 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:611 msgid "" "Order Cancelled: market order {order_book_id} fill {filled_volume} " "actually" @@ -668,14 +803,18 @@ msgid "" "matching_type is 'current_bar'." msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_simulation/mod.py:107 +#: rqalpha/mod/rqalpha_mod_sys_simulation/mod.py:119 msgid "NO account_type = ({}) in {}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:53 +#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:56 msgid "cancel_order function is not supported in signal mode" msgstr "" +#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:75 +msgid "Order Cancelled: current bar [{order_book_id}] miss market data." +msgstr "" + #: rqalpha/mod/rqalpha_mod_sys_simulation/simulation_broker.py:108 msgid "unsupported position_effect {}" msgstr "" @@ -688,7 +827,7 @@ msgstr "" msgid "Order Rejected: {order_book_id} can not match. Market close." msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_simulation/simulation_broker.py:184 +#: rqalpha/mod/rqalpha_mod_sys_simulation/simulation_broker.py:185 msgid "{order_book_id} should be subscribed when frequency is tick." msgstr "" @@ -716,13 +855,13 @@ msgstr "" msgid "invalid slippage rate value {} which cause price <= 0" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_transaction_cost/mod.py:30 +#: rqalpha/mod/rqalpha_mod_sys_transaction_cost/mod.py:42 msgid "" "invalid commission multiplier or tax multiplier value: value range is [0," " +∞)" msgstr "" -#: rqalpha/model/bar.py:371 +#: rqalpha/model/bar.py:372 msgid "id_or_symbols {} does not exist" msgstr "" @@ -733,117 +872,113 @@ msgid "" " {tax}, {frozen_price}" msgstr "" -#: rqalpha/portfolio/__init__.py:67 +#: rqalpha/portfolio/__init__.py:71 msgid "invalid init position {order_book_id}: no valid price at {date}" msgstr "" -#: rqalpha/portfolio/__init__.py:267 +#: rqalpha/portfolio/__init__.py:278 rqalpha/portfolio/__init__.py:291 msgid "invalid account type {}, choose in {}" msgstr "" -#: rqalpha/portfolio/__init__.py:271 +#: rqalpha/portfolio/__init__.py:282 msgid "Cash add {}. units {} become to {}" msgstr "" -#: rqalpha/portfolio/account.py:313 +#: rqalpha/portfolio/account.py:352 msgid "Trigger Forced Liquidation, current total_value is 0" msgstr "" -#: rqalpha/portfolio/account.py:457 +#: rqalpha/portfolio/account.py:502 rqalpha/portfolio/account.py:521 msgid "insufficient cash, current {}, target withdrawal {}" msgstr "" -#: rqalpha/portfolio/position.py:160 -msgid "invalid price of {order_book_id}: {price}" -msgstr "" - -#: rqalpha/utils/arg_checker.py:50 +#: rqalpha/utils/arg_checker.py:51 msgid "" "function {}: invalid {} argument, expect a value of type {}, got {} " "(type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:58 +#: rqalpha/utils/arg_checker.py:59 msgid "valid order_book_id/instrument" msgstr "" -#: rqalpha/utils/arg_checker.py:61 +#: rqalpha/utils/arg_checker.py:62 msgid "valid stock order_book_id/instrument" msgstr "" -#: rqalpha/utils/arg_checker.py:64 +#: rqalpha/utils/arg_checker.py:65 msgid "valid future order_book_id/instrument" msgstr "" -#: rqalpha/utils/arg_checker.py:67 +#: rqalpha/utils/arg_checker.py:68 msgid "listed order_book_id/instrument" msgstr "" -#: rqalpha/utils/arg_checker.py:70 +#: rqalpha/utils/arg_checker.py:71 msgid "function {}: invalid {} argument, expected a {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:96 +#: rqalpha/utils/arg_checker.py:97 msgid "" "function {}: invalid {} argument, expected instrument with types {}, got " "instrument with type {}" msgstr "" -#: rqalpha/utils/arg_checker.py:136 +#: rqalpha/utils/arg_checker.py:137 msgid "function {}: invalid {} argument, expect a number, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:151 +#: rqalpha/utils/arg_checker.py:160 msgid "function {}: invalid {} argument, valid: {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:165 +#: rqalpha/utils/arg_checker.py:174 msgid "function {}: invalid {} argument, valid fields are {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:177 +#: rqalpha/utils/arg_checker.py:186 msgid "function {}: invalid field {}, valid fields are {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:183 rqalpha/utils/arg_checker.py:198 +#: rqalpha/utils/arg_checker.py:192 rqalpha/utils/arg_checker.py:207 msgid "" "function {}: invalid {} argument, expect a string or a list of string, " "got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:225 rqalpha/utils/arg_checker.py:230 +#: rqalpha/utils/arg_checker.py:234 rqalpha/utils/arg_checker.py:239 msgid "function {}: invalid {} argument, expect a valid date, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:241 +#: rqalpha/utils/arg_checker.py:250 msgid "function {}: invalid {} argument, expect a value >= {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:251 +#: rqalpha/utils/arg_checker.py:260 msgid "function {}: invalid {} argument, expect a value > {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:261 +#: rqalpha/utils/arg_checker.py:270 msgid "function {}: invalid {} argument, expect a value <= {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:272 +#: rqalpha/utils/arg_checker.py:281 msgid "function {}: invalid {} argument, expect a value < {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:289 +#: rqalpha/utils/arg_checker.py:298 msgid "" "function {}: invalid {} argument, interval should be in form of '1d', " "'3m', '4q', '2y', got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:325 +#: rqalpha/utils/arg_checker.py:334 msgid "" "function {}: invalid {} argument, should be entity like " "Fundamentals.balance_sheet.total_equity, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:344 +#: rqalpha/utils/arg_checker.py:353 msgid "" "function {}: invalid {} argument, frequency should be in form of '1m', " "'5m', '1d', '1w' got {} (type: {})" diff --git a/requirements.txt b/requirements.txt index cb64365cb..987ed4688 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ simplejson >=3.10.0 dill ==0.2.5 PyYAML >=3.12 tabulate -rqrisk >=0.0.14 +rqrisk >=1.0.8 h5py matplotlib >=1.5.1 ; python_version >= '3.6' matplotlib >=1.5.1,<=3.0.3 ; python_version == '3.5' diff --git a/rqalpha/_version.py b/rqalpha/_version.py index 37e65fc17..d9c373d5d 100644 --- a/rqalpha/_version.py +++ b/rqalpha/_version.py @@ -5,8 +5,9 @@ # directories (produced by setup.py build) will contain a much shorter file # that just contains the computed version number. -# This file is released into the public domain. Generated by -# versioneer-0.18 (https://github.com/warner/python-versioneer) +# This file is released into the public domain. +# Generated by versioneer-0.28 +# https://github.com/python-versioneer/python-versioneer """Git implementation of _version.py.""" @@ -15,6 +16,8 @@ import re import subprocess import sys +from typing import Callable, Dict +import functools def get_keywords(): @@ -40,8 +43,8 @@ def get_config(): # _version.py cfg = VersioneerConfig() cfg.VCS = "git" - cfg.style = "pep440-ricequant" - cfg.tag_prefix = "release/" + cfg.style = "pep440" + cfg.tag_prefix = "" cfg.parentdir_prefix = "rqalpha" cfg.versionfile_source = "rqalpha/_version.py" cfg.verbose = False @@ -52,12 +55,12 @@ class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" -LONG_VERSION_PY = {} -HANDLERS = {} +LONG_VERSION_PY: Dict[str, str] = {} +HANDLERS: Dict[str, Dict[str, Callable]] = {} def register_vcs_handler(vcs, method): # decorator - """Decorator to mark a method as the handler for a particular VCS.""" + """Create decorator to mark a method as the handler of a VCS.""" def decorate(f): """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: @@ -71,17 +74,25 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): """Call the given command(s).""" assert isinstance(commands, list) - p = None - for c in commands: + process = None + + popen_kwargs = {} + if sys.platform == "win32": + # This hides the console window if pythonw.exe is used + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + popen_kwargs["startupinfo"] = startupinfo + + for command in commands: try: - dispcmd = str([c] + args) + dispcmd = str([command] + args) # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) + process = subprocess.Popen([command] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None), **popen_kwargs) break - except EnvironmentError: + except OSError: e = sys.exc_info()[1] if e.errno == errno.ENOENT: continue @@ -93,15 +104,13 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, if verbose: print("unable to find command, tried %s" % (commands,)) return None, None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: + stdout = process.communicate()[0].strip().decode() + if process.returncode != 0: if verbose: print("unable to run %s (error)" % dispcmd) print("stdout was %s" % stdout) - return None, p.returncode - return stdout, p.returncode + return None, process.returncode + return stdout, process.returncode def versions_from_parentdir(parentdir_prefix, root, verbose): @@ -113,15 +122,14 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): """ rootdirs = [] - for i in range(3): + for _ in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): return {"version": dirname[len(parentdir_prefix):], "full-revisionid": None, "dirty": False, "error": None, "date": None} - else: - rootdirs.append(root) - root = os.path.dirname(root) # up a level + rootdirs.append(root) + root = os.path.dirname(root) # up a level if verbose: print("Tried directories %s but none started with prefix %s" % @@ -138,22 +146,21 @@ def git_get_keywords(versionfile_abs): # _version.py. keywords = {} try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - f.close() - except EnvironmentError: + with open(versionfile_abs, "r") as fobj: + for line in fobj: + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + except OSError: pass return keywords @@ -161,10 +168,14 @@ def git_get_keywords(versionfile_abs): @register_vcs_handler("git", "keywords") def git_versions_from_keywords(keywords, tag_prefix, verbose): """Get version information from git keywords.""" - if not keywords: - raise NotThisMethod("no keywords at all, weird") + if "refnames" not in keywords: + raise NotThisMethod("Short version file found") date = keywords.get("date") if date is not None: + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] + # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 # -like" string, which we must then edit to make compliant), because @@ -177,11 +188,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) + refs = {r.strip() for r in refnames.strip("()").split(",")} # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d @@ -190,7 +201,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) + tags = {r for r in refs if re.search(r'\d', r)} if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: @@ -199,6 +210,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): r = ref[len(tag_prefix):] + # Filter out refs that exactly match prefix or that don't start + # with a number once the prefix is stripped (mostly a concern + # when prefix is '') + if not re.match(r'\d', r): + continue if verbose: print("picking %s" % r) return {"version": r, @@ -214,7 +230,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): @register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): +def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* @@ -225,8 +241,15 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=True) + # GIT_DIR can interfere with correct operation of Versioneer. + # It may be intended to be passed to the Versioneer-versioned project, + # but that should not change where we get our version from. + env = os.environ.copy() + env.pop("GIT_DIR", None) + runner = functools.partial(runner, env=env) + + _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=not verbose) if rc != 0: if verbose: print("Directory %s not under git control" % root) @@ -234,15 +257,15 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%s*" % tag_prefix], - cwd=root) + describe_out, rc = runner(GITS, [ + "describe", "--tags", "--dirty", "--always", "--long", + "--match", f"{tag_prefix}[[:digit:]]*" + ], cwd=root) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() - full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() @@ -252,6 +275,39 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None + branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], + cwd=root) + # --abbrev-ref was added in git-1.6.3 + if rc != 0 or branch_name is None: + raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") + branch_name = branch_name.strip() + + if branch_name == "HEAD": + # If we aren't exactly on a branch, pick a branch which represents + # the current commit. If all else fails, we are on a branchless + # commit. + branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) + # --contains was added in git-1.5.4 + if rc != 0 or branches is None: + raise NotThisMethod("'git branch --contains' returned error") + branches = branches.split("\n") + + # Remove the first line if we're running detached + if "(" in branches[0]: + branches.pop(0) + + # Strip off the leading "* " from the list of branches. + branches = [branch[2:] for branch in branches] + if "master" in branches: + branch_name = "master" + elif not branches: + branch_name = None + else: + # Pick the first branch that is returned. Good or bad. + branch_name = branches[0] + + pieces["branch"] = branch_name + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out @@ -268,7 +324,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # TAG-NUM-gHEX mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) if not mo: - # unparseable. Maybe git-describe is misbehaving? + # unparsable. Maybe git-describe is misbehaving? pieces["error"] = ("unable to parse git-describe output: '%s'" % describe_out) return pieces @@ -293,13 +349,14 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): else: # HEX: no tags pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) - pieces["distance"] = int(count_out) # total number of commits + out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) + pieces["distance"] = len(out.split()) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], - cwd=root)[0].strip() + date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces @@ -337,19 +394,67 @@ def render_pep440(pieces): return rendered -def render_pep440_pre(pieces): - """TAG[.post.devDISTANCE] -- No -dirty. +def render_pep440_branch(pieces): + """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . + + The ".dev0" means not master branch. Note that .dev0 sorts backwards + (a feature branch will appear "older" than the master branch). Exceptions: - 1: no tags. 0.post.devDISTANCE + 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0" + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def pep440_split_post(ver): + """Split pep440 version string at the post-release segment. + + Returns the release segments before the post-release and the + post-release version number (or -1 if no post-release segment is present). + """ + vc = str.split(ver, ".post") + return vc[0], int(vc[1] or 0) if len(vc) == 2 else None + + +def render_pep440_pre(pieces): + """TAG[.postN.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post0.devDISTANCE + """ + if pieces["closest-tag"]: if pieces["distance"]: - rendered += ".post.dev%d" % pieces["distance"] + # update the post release segment + tag_version, post_version = pep440_split_post(pieces["closest-tag"]) + rendered = tag_version + if post_version is not None: + rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) + else: + rendered += ".post0.dev%d" % (pieces["distance"]) + else: + # no commits, use the tag as the version + rendered = pieces["closest-tag"] else: # exception #1 - rendered = "0.post.dev%d" % pieces["distance"] + rendered = "0.post0.dev%d" % pieces["distance"] return rendered @@ -380,25 +485,32 @@ def render_pep440_post(pieces): return rendered -def render_pep440_ricequant(pieces): - """TAG[.devDISTANCE.gHEX[.dirty]] . +def render_pep440_post_branch(pieces): + """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . + + The ".dev0" means not master branch. Exceptions: - 1: no tags. 0.devDISTANCE.gHEX[.dirty] + 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: - rendered += ".dev%d" % pieces["distance"] - rendered += ".g%s" % pieces["short"] + rendered += ".post%d" % pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" else: # exception #1 - rendered = "0.dev%d" % pieces["distance"] - rendered += ".g%s" % pieces["short"] - - if pieces["dirty"]: - rendered += ".dirty" - + rendered = "0.post%d" % pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" return rendered @@ -407,7 +519,7 @@ def render_pep440_old(pieces): The ".dev0" means dirty. - Eexceptions: + Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: @@ -478,14 +590,16 @@ def render(pieces, style): if style == "pep440": rendered = render_pep440(pieces) + elif style == "pep440-branch": + rendered = render_pep440_branch(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) + elif style == "pep440-post-branch": + rendered = render_pep440_post_branch(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) - elif style == 'pep440-ricequant': - rendered = render_pep440_ricequant(pieces) elif style == "git-describe": rendered = render_git_describe(pieces) elif style == "git-describe-long": @@ -519,7 +633,7 @@ def get_versions(): # versionfile_source is the relative path from the top of the source # tree (where the .git directory might live) to this file. Invert # this to find the root from __file__. - for i in cfg.versionfile_source.split('/'): + for _ in cfg.versionfile_source.split('/'): root = os.path.dirname(root) except NameError: return {"version": "0+unknown", "full-revisionid": None, diff --git a/rqalpha/config.yml b/rqalpha/config.yml index 11f2239e1..2157ac47b 100644 --- a/rqalpha/config.yml +++ b/rqalpha/config.yml @@ -39,6 +39,8 @@ base: future_info: {} # 强平 forced_liquidation: true + # 是否开启期货历史交易参数进行回测,默认为 False + futures_time_series_trading_parameters: false extra: diff --git a/rqalpha/data/base_data_source/data_source.py b/rqalpha/data/base_data_source/data_source.py index fe42cc091..b5118ee51 100644 --- a/rqalpha/data/base_data_source/data_source.py +++ b/rqalpha/data/base_data_source/data_source.py @@ -16,7 +16,6 @@ # 详细的授权流程,请联系 public@ricequant.com 获取。 import os import pickle -from functools import lru_cache from datetime import date, datetime, timedelta from itertools import groupby from typing import Dict, Iterable, List, Optional, Sequence, Union @@ -24,22 +23,24 @@ import numpy as np import pandas as pd import six -from rqalpha.const import INSTRUMENT_TYPE, TRADING_CALENDAR_TYPE +from rqalpha.utils.i18n import gettext as _ +from rqalpha.const import INSTRUMENT_TYPE, TRADING_CALENDAR_TYPE, DEFAULT_ACCOUNT_TYPE from rqalpha.interface import AbstractDataSource from rqalpha.model.instrument import Instrument from rqalpha.utils.datetime_func import (convert_date_to_int, convert_int_to_date, convert_int_to_datetime) -from rqalpha.utils.exception import RQInvalidArgument +from rqalpha.utils.exception import RQInvalidArgument, RQDatacVersionTooLow from rqalpha.utils.functools import lru_cache from rqalpha.utils.typing import DateLike from rqalpha.environment import Environment - +from rqalpha.data.bundle import update_futures_trading_parameters +from rqalpha.utils.logger import user_system_log from rqalpha.data.base_data_source.adjust import FIELDS_REQUIRE_ADJUSTMENT, adjust_bars from rqalpha.data.base_data_source.storage_interface import (AbstractCalendarStore, AbstractDateSet, AbstractDayBarStore, AbstractDividendStore, AbstractInstrumentStore) from rqalpha.data.base_data_source.storages import (DateSet, DayBarStore, DividendStore, ExchangeTradingCalendarStore, FutureDayBarStore, - FutureInfoStore, InstrumentStore, + FutureInfoStore, FuturesTradingParametersStore,InstrumentStore, ShareTransformationStore, SimpleFactorStore, YieldCurveStore) @@ -71,7 +72,8 @@ class BaseDataSource(AbstractDataSource): INSTRUMENT_TYPE.PUBLIC_FUND, ) - def __init__(self, path, custom_future_info): + def __init__(self, path, custom_future_info, futures_time_series_trading_parameters=False, end_date=None): + # type: (str, dict, bool, date) -> None if not os.path.exists(path): raise RuntimeError('bundle path {} not exist'.format(os.path.abspath(path))) @@ -86,20 +88,26 @@ def _p(name): INSTRUMENT_TYPE.ETF: funds_day_bar_store, INSTRUMENT_TYPE.LOF: funds_day_bar_store } # type: Dict[INSTRUMENT_TYPE, AbstractDayBarStore] - + + self._futures_trading_parameters_store = None self._future_info_store = FutureInfoStore(_p("future_info.json"), custom_future_info) - + self._instruments_stores = {} # type: Dict[INSTRUMENT_TYPE, AbstractInstrumentStore] self._ins_id_or_sym_type_map = {} # type: Dict[str, INSTRUMENT_TYPE] instruments = [] + with open(_p('instruments.pk'), 'rb') as f: for i in pickle.load(f): if i["type"] == "Future" and Instrument.is_future_continuous_contract(i["order_book_id"]): i["listed_date"] = datetime(1990, 1, 1) - instruments.append(Instrument(i, lambda i: self._future_info_store.get_future_info(i)["tick_size"])) + instruments.append(Instrument( + i, + lambda i: self._future_info_store.get_tick_size(i), + lambda i, dt: self.get_futures_trading_parameters(i, dt).long_margin_ratio, + lambda i, dt: self.get_futures_trading_parameters(i, dt).short_margin_ratio + )) for ins_type in self.DEFAULT_INS_TYPES: self.register_instruments_store(InstrumentStore(instruments, ins_type)) - dividend_store = DividendStore(_p('dividends.h5')) self._dividends = { INSTRUMENT_TYPE.CS: dividend_store, @@ -125,6 +133,20 @@ def _p(name): self._suspend_days = [DateSet(_p('suspended_days.h5'))] # type: List[AbstractDateSet] self._st_stock_days = DateSet(_p('st_stock_days.h5')) + if futures_time_series_trading_parameters: + try: + import rqdatac + except ImportError: + user_system_log.warn(_("RQDatac is not installed, \"config.base.futures_time_series_trading_parameters\" will be disabled.")) + else: + try: + update_futures_trading_parameters(path, end_date) + except (rqdatac.share.errors.PermissionDenied, RQDatacVersionTooLow): + user_system_log.warn(_("RQDatac does not have permission to obtain futures histrical trading parameters, \"config.base.futures_time_series_trading_parameters\" will be disabled.")) + else: + file = os.path.join(path, "futures_trading_parameters.h5") + self._futures_trading_parameters_store = FuturesTradingParametersStore(file, custom_future_info) + def register_day_bar_store(self, instrument_type, store): # type: (INSTRUMENT_TYPE, AbstractDayBarStore) -> None self._day_bars[instrument_type] = store @@ -359,8 +381,16 @@ def available_data_range(self, frequency): def get_yield_curve(self, start_date, end_date, tenor=None): return self._yield_curve.get_yield_curve(start_date, end_date, tenor=tenor) - def get_commission_info(self, instrument): - return self._future_info_store.get_future_info(instrument) + @lru_cache(1024) + def get_futures_trading_parameters(self, instrument, dt): + # type: (Instrument, datetime.date) -> FuturesTradingParameters + if self._futures_trading_parameters_store: + trading_parameters = self._futures_trading_parameters_store.get_futures_trading_parameters(instrument, dt) + if trading_parameters is None: + return self._future_info_store.get_future_info(instrument.order_book_id, instrument.underlying_symbol) + return trading_parameters + else: + return self._future_info_store.get_future_info(instrument.order_book_id, instrument.underlying_symbol) def get_merge_ticks(self, order_book_id_list, trading_date, last_dt=None): raise NotImplementedError diff --git a/rqalpha/data/base_data_source/storages.py b/rqalpha/data/base_data_source/storages.py index 73c263172..2998c04e4 100644 --- a/rqalpha/data/base_data_source/storages.py +++ b/rqalpha/data/base_data_source/storages.py @@ -23,7 +23,7 @@ from copy import copy from itertools import chain from contextlib import contextmanager -from typing import Dict, Iterable, Optional +from typing import Dict, Iterable, Optional, NamedTuple import h5py import numpy as np @@ -34,6 +34,7 @@ from rqalpha.model.instrument import Instrument from rqalpha.utils.datetime_func import convert_date_to_date_int from rqalpha.utils.i18n import gettext as _ +from rqalpha.utils.logger import user_system_log from .storage_interface import (AbstractCalendarStore, AbstractDateSet, AbstractDayBarStore, AbstractDividendStore, @@ -41,6 +42,18 @@ AbstractSimpleFactorStore) +class FuturesTradingParameters(NamedTuple): + """ + 数据类,用以存储期货交易参数数据 + """ + close_commission_ratio: float + close_commission_today_ratio: float + commission_type: str + open_commission_ratio: float + long_margin_ratio: float + short_margin_ratio: float + + class ExchangeTradingCalendarStore(AbstractCalendarStore): def __init__(self, f): self._f = f @@ -64,27 +77,52 @@ def __init__(self, f, custom_future_info): ) for item in json.load(json_file) } self._custom_data = custom_future_info - self._future_info = {} + if "margin_rate" not in self._default_data[next(iter(self._default_data))]: + raise RuntimeError(_("Your bundle data is too old, please use 'rqalpha update-bundle' or 'rqalpha download-bundle' to update it to lastest before using")) @classmethod def _process_future_info_item(cls, item): item["commission_type"] = cls.COMMISSION_TYPE_MAP[item["commission_type"]] return item - def get_future_info(self, instrument): - # type: (Instrument) -> Dict[str, float] - order_book_id = instrument.order_book_id + @lru_cache(1024) + def get_future_info(self, order_book_id, underlying_symbol): + # type: (str, str) -> FuturesTradingParameters + custom_info = self._custom_data.get(order_book_id) or self._custom_data.get(underlying_symbol) + info = self._default_data.get(order_book_id) or self._default_data.get(underlying_symbol) + if custom_info: + info = copy(info) or {} + info.update(custom_info) + elif not info: + raise NotImplementedError(_("unsupported future instrument {}").format(order_book_id)) + info = self._to_namedtuple(info) + return info + + def _to_namedtuple(self, info): + # type: (dict) -> FuturesTradingParameters + info['long_margin_ratio'], info['short_margin_ratio'] = info['margin_rate'], info['margin_rate'] + del info['margin_rate'], info['tick_size'] try: - return self._future_info[order_book_id] + del info['order_book_id'] except KeyError: - custom_info = self._custom_data.get(order_book_id) or self._custom_data.get(instrument.underlying_symbol) - info = self._default_data.get(order_book_id) or self._default_data.get(instrument.underlying_symbol) - if custom_info: - info = copy(info) or {} - info.update(custom_info) - elif not info: - raise NotImplementedError(_("unsupported future instrument {}").format(order_book_id)) - return self._future_info.setdefault(order_book_id, info) + del info['underlying_symbol'] + info = FuturesTradingParameters(**info) + return info + + @lru_cache(8) + def get_tick_size(self, instrument): + # type: (str, str) -> float + order_book_id = instrument.order_book_id + underlying_symbol = instrument.underlying_symbol + custom_info = self._custom_data.get(order_book_id) or self._custom_data.get(underlying_symbol) + info = self._default_data.get(order_book_id) or self._custom_data.get(underlying_symbol) + if custom_info: + info = copy(info) or {} + info.update(custom_info) + elif not info: + raise NotImplementedError(_("unsupported future instrument {}".format(order_book_id))) + tick_size = info['tick_size'] + return tick_size class InstrumentStore(AbstractInstrumentStore): @@ -208,6 +246,72 @@ class FutureDayBarStore(DayBarStore): DEFAULT_DTYPE = np.dtype(DayBarStore.DEFAULT_DTYPE.descr + [("open_interest", ' FuturesTradingParameters or None + dt = convert_date_to_date_int(dt) + if dt < self.FUTURES_TRADING_PARAMETERS_START_DATE: + return None + order_book_id = instrument.order_book_id + underlying_symbol = instrument.underlying_symbol + data = self.get_futures_trading_parameters_all_time(order_book_id) + if data is None: + return None + else: + arr = data[data['datetime'] == dt] + if len(arr) == 0: + if dt >= convert_date_to_date_int(instrument.listed_date) and dt <= convert_date_to_date_int(instrument.de_listed_date): + user_system_log.info("Historical futures trading parameters are abnormal, the lastst parameters will be used for calculations.\nPlease contract RiceQuant to repair: 0755-26569969") + return None + custom_info = self._custom_data.get(order_book_id) or self._custom_data.get(underlying_symbol) + if custom_info: + arr[0] = self.set_custom_info(arr[0], custom_info) + futures_trading_parameters = self._to_namedtuple(arr[0]) + return futures_trading_parameters + + @lru_cache(1024) + def get_futures_trading_parameters_all_time(self, order_book_id): + # type: (str) -> numpy.ndarray or None + with h5_file(self._path) as h5: + try: + data = h5[order_book_id][:] + except KeyError: + return None + return data + + def set_custom_info(self, arr, custom_info): + for field in custom_info: + if field == "commission_type": + if custom_info[field] == COMMISSION_TYPE.BY_MONEY: + value = 0 + elif custom_info[field] == COMMISSION_TYPE.BY_VOLUME: + value = 1 + else: + value = custom_info[field] + arr[field] = value + return arr + + def _to_namedtuple(self, arr): + # type: (numpy.void) -> FuturesTradingParameters + dic = dict(zip(arr.dtype.names, arr)) + del dic['datetime'] + dic["commission_type"] = self.COMMISSION_TYPE_MAP[dic['commission_type']] + futures_trading_parameters = FuturesTradingParameters(**dic) + return futures_trading_parameters + + class DividendStore(AbstractDividendStore): def __init__(self, path): self._path = path diff --git a/rqalpha/data/bundle.py b/rqalpha/data/bundle.py index 69159cfcc..a9be5d559 100644 --- a/rqalpha/data/bundle.py +++ b/rqalpha/data/bundle.py @@ -26,6 +26,9 @@ ProgressedTask) from rqalpha.utils.datetime_func import (convert_date_to_date_int, convert_date_to_int) +from rqalpha.utils.exception import RQDatacVersionTooLow +from rqalpha.utils.i18n import gettext as _ +from rqalpha.utils.logger import system_log, user_system_log START_DATE = 20050104 END_DATE = 29991231 @@ -136,42 +139,76 @@ def gen_share_transformation(d): def gen_future_info(d): future_info_file = os.path.join(d, 'future_info.json') + def _need_to_recreate(): + if not os.path.exists(future_info_file): + return + else: + with open(future_info_file, "r") as f: + all_futures_info = json.load(f) + if "margin_rate" not in all_futures_info[0]: + return True + + def update_margin_rate(file): + all_instruments_data = rqdatac.all_instruments("Future") + with open(file, "r") as f: + all_futures_info = json.load(f) + new_all_futures_info = [] + for future_info in all_futures_info: + if "order_book_id" in future_info: + future_info["margin_rate"] = all_instruments_data[all_instruments_data["order_book_id"] == future_info["order_book_id"]].iloc[0].margin_rate + elif "underlying_symbol" in future_info: + dominant = rqdatac.futures.get_dominant(future_info["underlying_symbol"])[-1] + future_info["margin_rate"] = all_instruments_data[all_instruments_data["order_book_id"] == dominant].iloc[0].margin_rate + new_all_futures_info.append(future_info) + os.remove(file) + with open(file, "w") as f: + json.dump(new_all_futures_info, f, separators=(',', ':'), indent=2) + + if (_need_to_recreate()): update_margin_rate(future_info_file) + + # 新增 hard_code 的种类时,需要同时修改rqalpha.data.base_data_source.storages.FutureInfoStore.data_compatible中的内容 hard_code = [ {'underlying_symbol': 'TC', 'close_commission_ratio': 4.0, 'close_commission_today_ratio': 0.0, 'commission_type': "by_volume", 'open_commission_ratio': 4.0, + 'margin_rate': 0.05, 'tick_size': 0.2}, {'underlying_symbol': 'ER', 'close_commission_ratio': 2.5, 'close_commission_today_ratio': 2.5, 'commission_type': "by_volume", 'open_commission_ratio': 2.5, + 'margin_rate': 0.05, 'tick_size': 1.0}, {'underlying_symbol': 'WS', 'close_commission_ratio': 2.5, 'close_commission_today_ratio': 0.0, 'commission_type': "by_volume", 'open_commission_ratio': 2.5, + 'margin_rate': 0.05, 'tick_size': 1.0}, {'underlying_symbol': 'RO', 'close_commission_ratio': 2.5, 'close_commission_today_ratio': 0.0, 'commission_type': "by_volume", 'open_commission_ratio': 2.5, + 'margin_rate': 0.05, 'tick_size': 2.0}, {'underlying_symbol': 'ME', 'close_commission_ratio': 1.4, 'close_commission_today_ratio': 0.0, 'commission_type': "by_volume", 'open_commission_ratio': 1.4, + 'margin_rate': 0.06, 'tick_size': 1.0}, {'underlying_symbol': 'WT', 'close_commission_ratio': 5.0, 'close_commission_today_ratio': 5.0, 'commission_type': "by_volume", 'open_commission_ratio': 5.0, + 'margin_rate': 0.05, 'tick_size': 1.0}, ] @@ -198,18 +235,21 @@ def gen_future_info(d): symbol_list.append(info["underlying_symbol"]) futures_order_book_id = rqdatac.all_instruments(type='Future')['order_book_id'].unique() + commission_df = rqdatac.futures.get_commission_margin() for future in futures_order_book_id: underlying_symbol = re.match(r'^[a-zA-Z]*', future).group() if future in future_list: continue future_dict = {} - commission = rqdatac.futures.get_commission_margin(future) + commission = commission_df[commission_df['order_book_id'] == future] if not commission.empty: future_dict['order_book_id'] = future commission = commission.iloc[0] for p in param: future_dict[p] = commission[p] - future_dict['tick_size'] = rqdatac.instruments(future).tick_size() + instruemnts_data = rqdatac.instruments(future) + future_dict['margin_rate'] = instruemnts_data.margin_rate + future_dict['tick_size'] = instruemnts_data.tick_size() elif underlying_symbol in symbol_list: continue else: @@ -220,11 +260,13 @@ def gen_future_info(d): except AttributeError: # FIXME: why get_dominant return None??? continue - commission = rqdatac.futures.get_commission_margin(dominant).iloc[0] + commission = commission_df[commission_df['order_book_id'] == dominant].iloc[0] for p in param: future_dict[p] = commission[p] - future_dict['tick_size'] = rqdatac.instruments(dominant).tick_size() + instruemnts_data = rqdatac.instruments(dominant) + future_dict['margin_rate'] = instruemnts_data.margin_rate + future_dict['tick_size'] = instruemnts_data.tick_size() all_futures_info.append(future_dict) with open(os.path.join(d, 'future_info.json'), 'w') as f: @@ -395,3 +437,190 @@ def update_bundle(path, create, enable_compression=False, concurrency=1): executor.submit(GenerateFileTask(func), path) for file, order_book_id, field in day_bar_args: executor.submit(_DayBarTask(order_book_id), os.path.join(path, file), field, **kwargs) + + +FUTURES_TRADING_PARAMETERS_FIELDS = ["long_margin_ratio", "short_margin_ratio", "commission_type", "open_commission", "close_commission", "close_commission_today"] +TRADING_PARAMETERS_START_DATE = 20100401 +FUTURES_TRADING_PARAMETERS_FILE = "futures_trading_parameters.h5" + + +class FuturesTradingParametersTask(object): + def __init__(self, order_book_ids, underlying_symbols): + self._order_book_ids = order_book_ids + self._underlying_symbols = underlying_symbols + + def __call__(self, path, fields, end_date): + if rqdatac.__version__ < '2.11.12': + raise RQDatacVersionTooLow(_("RQAlpha already supports backtesting using futures historical margins and rates, please upgrade RQDatac to version 2.11.12 and above to use it")) + + if not os.path.exists(path): + self.generate_futures_trading_parameters(path, fields, end_date) + else: + self.update_futures_trading_parameters(path, fields, end_date) + + def generate_futures_trading_parameters(self, path, fields, end_date, recreate_futures_list=None): + # type: (str, list, datetime.date, list) -> None + if not recreate_futures_list: + system_log.info(_("Futures historical trading parameters data is being updated, please wait......")) + order_book_ids = self._order_book_ids + if recreate_futures_list: + order_book_ids = recreate_futures_list + df = rqdatac.futures.get_trading_parameters(order_book_ids, TRADING_PARAMETERS_START_DATE, end_date, fields) + if not (df is None or df.empty): + df.dropna(axis=0, how="all") + df.reset_index(inplace=True) + df['datetime'] = df['trading_date'].map(convert_date_to_date_int) + del df["trading_date"] + df['commission_type'] = df['commission_type'].map(self.set_commission_type) + df.rename(columns={ + 'close_commission': "close_commission_ratio", + 'close_commission_today': "close_commission_today_ratio", + 'open_commission': 'open_commission_ratio' + }, inplace=True) + df.set_index(["order_book_id", "datetime"], inplace=True) + df.sort_index(inplace=True) + with h5py.File(path, "w") as h5: + for order_book_id in df.index.levels[0]: + h5.create_dataset(order_book_id, data=df.loc[order_book_id].to_records()) + # 更新期货连续合约的历史交易参数数据(当函数执行目的为补充上次未正常更新的数据时,不需要执行此段逻辑) + if recreate_futures_list is None: + with h5py.File(path, "a") as h5: + df = rqdatac.all_instruments("Future") + for underlying_symbol in self._underlying_symbols: + futures_continuous_contract = df[(df['underlying_symbol'] == underlying_symbol) & (df["listed_date"] == '0000-00-00')].order_book_id.tolist() + s = rqdatac.futures.get_dominant(underlying_symbol, TRADING_PARAMETERS_START_DATE, end_date) + if (s is None or s.empty): + continue + s = s.to_frame().reset_index() + s['date'] = s['date'].map(convert_date_to_date_int) + s.set_index(['date'], inplace=True) + trading_parameters_list = [] + for date in s.index: + try: + data = h5[s['dominant'][date]][:] + except KeyError: + continue + trading_parameters = data[data['datetime'] == date] + if len(trading_parameters) != 0: + trading_parameters_list.append(trading_parameters[0]) + data = np.array(trading_parameters_list) + for order_book_id in futures_continuous_contract: + h5.create_dataset(order_book_id, data=data) + + def update_futures_trading_parameters(self, path, fields, end_date): + # type: (str, list, datetime.date) -> None + try: + h5 = h5py.File(path, "a") + h5.close() + except OSError as e: + raise OSError(_("File {} update failed, if it is using, please update later, or you can delete then update again".format(path))) from e + last_date = self.get_h5_last_date(path) + recreate_futures_list = self.get_recreate_futures_list(path, last_date) + if recreate_futures_list: + self.generate_futures_trading_parameters(path, fields, last_date, recreate_futures_list=recreate_futures_list) + if end_date > last_date: + system_log.info(_("Futures historical trading parameters data is being updated, please wait......")) + if rqdatac.get_previous_trading_date(end_date) == last_date: + return + else: + start_date = rqdatac.get_next_trading_date(last_date) + df = rqdatac.futures.get_trading_parameters(self._order_book_ids, start_date, end_date, fields) + if not(df is None or df.empty): + df = df.dropna(axis=0, how="all") + df.reset_index(inplace=True) + df['datetime'] = df['trading_date'].map(convert_date_to_date_int) + del [df['trading_date']] + df['commission_type'] = df['commission_type'].map(self.set_commission_type) + df.rename(columns={ + 'close_commission': "close_commission_ratio", + 'close_commission_today': "close_commission_today_ratio", + 'open_commission': 'open_commission_ratio' + }, inplace=True) + df.set_index(['order_book_id', 'datetime'], inplace=True) + with h5py.File(path, "a") as h5: + for order_book_id in df.index.levels[0]: + if order_book_id in h5: + data = np.array( + [tuple(i) for i in chain(h5[order_book_id][:], df.loc[order_book_id].to_records())], + dtype=h5[order_book_id].dtype + ) + del h5[order_book_id] + h5.create_dataset(order_book_id, data=data) + else: + h5.create_dataset(order_book_id, data=df.loc[order_book_id].to_records()) + # 更新期货连续合约历史交易参数 + with h5py.File(path, "a") as h5: + df = rqdatac.all_instruments("Future") + for underlying_symbol in self._underlying_symbols: + futures_continuous_contract = df[(df['underlying_symbol'] == underlying_symbol) & (df["listed_date"] == '0000-00-00')].order_book_id.tolist() + s = rqdatac.futures.get_dominant(underlying_symbol, start_date, end_date) + if (s is None or s.empty): + continue + s = s.to_frame().reset_index() + s['date'] = s['date'].map(convert_date_to_date_int) + s.set_index(['date'], inplace=True) + trading_parameters_list = [] + for date in s.index: + try: + data = h5[s['dominant'][date]][:] + except KeyError: + continue + trading_parameters = data[data['datetime'] == date] + if len(trading_parameters) != 0: + trading_parameters_list.append(trading_parameters[0]) + for order_book_id in futures_continuous_contract: + if order_book_id in h5: + data = np.array( + [tuple(i) for i in chain(h5[order_book_id][:], trading_parameters_list)], + dtype=h5[order_book_id].dtype + ) + del h5[order_book_id] + h5.create_dataset(order_book_id, data=data) + else: + h5.create_dataset(order_book_id, data=np.array(trading_parameters)) + + def set_commission_type(self, commission_type): + if commission_type == "by_money": + commission_type = 0 + elif commission_type == "by_volume": + commission_type = 1 + return commission_type + + def get_h5_last_date(self, path): + last_date = TRADING_PARAMETERS_START_DATE + with h5py.File(path, "r") as h5: + for key in h5.keys(): + if int(h5[key]['datetime'][-1]) > last_date: + last_date = h5[key]['datetime'][-1] + last_date = datetime.datetime.strptime(str(last_date), "%Y%m%d").date() + return last_date + + def get_recreate_futures_list(self, path, h5_last_date): + # type: (str, datetime.date) -> list + """ + 用户在运行策略的过程中可能中断进程,进而可能导致在创建 h5 文件时,部分合约没有成功 download + 通过该函数,获取在上一次更新中因为异常而没有更新的合约 + """ + recreate_futures_list = [] + df = rqdatac.all_instruments("Future") + last_update_futures_list = df[(df['de_listed_date'] >= str(TRADING_PARAMETERS_START_DATE)) & (df['listed_date'] <= h5_last_date.strftime("%Y%m%d"))].order_book_id.to_list() + with h5py.File(path, "r") as h5: + h5_order_book_ids = h5.keys() + for order_book_id in last_update_futures_list: + if order_book_id in h5_order_book_ids: + continue + else: + recreate_futures_list.append(order_book_id) + return recreate_futures_list + + +def update_futures_trading_parameters(path, end_date): + # type: (str, datetime.date) -> None + df = rqdatac.all_instruments("Future") + order_book_ids = (df[df['de_listed_date'] >= str(TRADING_PARAMETERS_START_DATE)]).order_book_id.tolist() + underlying_symbols = list(set((df[df['de_listed_date'] >= str(TRADING_PARAMETERS_START_DATE)]).underlying_symbol.tolist())) + FuturesTradingParametersTask(order_book_ids, underlying_symbols)( + os.path.join(path, FUTURES_TRADING_PARAMETERS_FILE), + FUTURES_TRADING_PARAMETERS_FIELDS, + end_date + ) diff --git a/rqalpha/data/data_proxy.py b/rqalpha/data/data_proxy.py index e2337a02c..d43d55927 100644 --- a/rqalpha/data/data_proxy.py +++ b/rqalpha/data/data_proxy.py @@ -240,9 +240,10 @@ def tick_fields_for(ins): def available_data_range(self, frequency): return self._data_source.available_data_range(frequency) - def get_commission_info(self, order_book_id): + def get_futures_trading_parameters(self, order_book_id, dt): + # type: (str, datetime.date) -> FuturesTradingParameters instrument = self.instruments(order_book_id) - return self._data_source.get_commission_info(instrument) + return self._data_source.get_futures_trading_parameters(instrument, dt) def get_merge_ticks(self, order_book_id_list, trading_date, last_dt=None): return self._data_source.get_merge_ticks(order_book_id_list, trading_date, last_dt) diff --git a/rqalpha/environment.py b/rqalpha/environment.py index 02673a480..dc3a01d39 100644 --- a/rqalpha/environment.py +++ b/rqalpha/environment.py @@ -186,4 +186,3 @@ def can_submit_order(self, order): if not v.can_submit_order(order, account): return False return True - diff --git a/rqalpha/interface.py b/rqalpha/interface.py index 653cd8f1a..5b494f8c1 100644 --- a/rqalpha/interface.py +++ b/rqalpha/interface.py @@ -454,9 +454,9 @@ def available_data_range(self, frequency): """ raise NotImplementedError - def get_commission_info(self, instrument): + def get_futures_trading_parameters(self, instrument): """ - 获取合约的手续费信息 + 获取期货合约的时序手续费信息 :param instrument: :return: """ diff --git a/rqalpha/main.py b/rqalpha/main.py index 5f397cb29..bf55616ff 100644 --- a/rqalpha/main.py +++ b/rqalpha/main.py @@ -144,7 +144,12 @@ def run(config, source_code=None, user_funcs=None): mod_handler.start_up() if not env.data_source: - env.set_data_source(BaseDataSource(config.base.data_bundle_path, getattr(config.base, "future_info", {}))) + env.set_data_source(BaseDataSource( + config.base.data_bundle_path, + getattr(config.base, "future_info", {}), + const.DEFAULT_ACCOUNT_TYPE.FUTURE in config.base.accounts and config.base.futures_time_series_trading_parameters, + config.base.end_date + )) if env.price_board is None: from rqalpha.data.bar_dict_price_board import BarDictPriceBoard env.price_board = BarDictPriceBoard() diff --git a/rqalpha/mod/rqalpha_mod_sys_accounts/position_model.py b/rqalpha/mod/rqalpha_mod_sys_accounts/position_model.py index 81a3cfdfc..f851bc9e6 100644 --- a/rqalpha/mod/rqalpha_mod_sys_accounts/position_model.py +++ b/rqalpha/mod/rqalpha_mod_sys_accounts/position_model.py @@ -28,6 +28,7 @@ from rqalpha.utils.logger import user_system_log from rqalpha.utils.class_helper import deprecated_property, cached_property from rqalpha.utils.i18n import gettext as _ +from rqalpha.core.events import EVENT, Event def _int_to_date(d): @@ -222,10 +223,15 @@ class FuturePosition(Position): @cached_property def contract_multiplier(self): return self._instrument.contract_multiplier - - @cached_property + + @property def margin_rate(self): - return self._instrument.margin_rate * self._env.config.base.margin_multiplier + # type: () -> float + if self.direction == POSITION_DIRECTION.LONG: + margin_ratio = self._instrument.get_long_margin_ratio(self._env.trading_dt.date()) + elif self.direction == POSITION_DIRECTION.SHORT: + margin_ratio = self._instrument.get_short_margin_ratio(self._env.trading_dt.date()) + return margin_ratio * self._env.config.base.margin_multiplier @property def equity(self): @@ -234,7 +240,8 @@ def equity(self): return self._quantity * (self.last_price - self._avg_price) * self.contract_multiplier * self._direction_factor @property - def margin(self) -> float: + def margin(self): + # rtpe: () -> float """ 保证金 = 持仓量 * 最新价 * 合约乘数 * 保证金率 """ @@ -309,6 +316,12 @@ def settlement(self, trading_date): )) self._quantity = self._old_quantity = 0 return delta_cash + + def post_settlement(self): + try: + del self.__dict__["margin_ratio"] + except KeyError: + pass class StockPositionProxy(PositionProxy): diff --git a/rqalpha/mod/rqalpha_mod_sys_analyser/mod.py b/rqalpha/mod/rqalpha_mod_sys_analyser/mod.py index 9a064e47f..587773139 100644 --- a/rqalpha/mod/rqalpha_mod_sys_analyser/mod.py +++ b/rqalpha/mod/rqalpha_mod_sys_analyser/mod.py @@ -150,7 +150,7 @@ def _subscribe_events(self, event): self._env.event_bus.add_listener(EVENT.TRADE, self._collect_trade) self._env.event_bus.add_listener(EVENT.ORDER_CREATION_PASS, self._collect_order) - self._env.event_bus.add_listener(EVENT.POST_SETTLEMENT, self._collect_daily) + self._env.event_bus.prepend_listener(EVENT.POST_SETTLEMENT, self._collect_daily) def _collect_trade(self, event): self._trades.append(self._to_trade_record(event.trade)) diff --git a/rqalpha/mod/rqalpha_mod_sys_risk/validators/cash_validator.py b/rqalpha/mod/rqalpha_mod_sys_risk/validators/cash_validator.py index 7830b0686..43c2d1503 100644 --- a/rqalpha/mod/rqalpha_mod_sys_risk/validators/cash_validator.py +++ b/rqalpha/mod/rqalpha_mod_sys_risk/validators/cash_validator.py @@ -24,7 +24,7 @@ def is_cash_enough(env, order, cash, warn=False): instrument = env.data_proxy.instrument(order.order_book_id) - cost_money = instrument.calc_cash_occupation(order.frozen_price, order.quantity, order.position_direction) + cost_money = instrument.calc_cash_occupation(order.frozen_price, order.quantity, order.position_direction, order.trading_datetime.date()) cost_money += env.get_order_transaction_cost(order) if cost_money <= cash: return True diff --git a/rqalpha/mod/rqalpha_mod_sys_transaction_cost/__init__.py b/rqalpha/mod/rqalpha_mod_sys_transaction_cost/__init__.py index fd5c8925f..718047f00 100644 --- a/rqalpha/mod/rqalpha_mod_sys_transaction_cost/__init__.py +++ b/rqalpha/mod/rqalpha_mod_sys_transaction_cost/__init__.py @@ -26,6 +26,8 @@ "futures_commission_multiplier": 1, # 印花倍率,即在默认的印花税基础上按该倍数进行调整,股票默认印花税为千分之一,单边收取 "tax_multiplier": 1, + # 是否使用回测当时时间点对应的真实印花税率 + "pit_tax": False, } cli_prefix = "mod__sys_transaction_cost__" @@ -83,6 +85,14 @@ ) ) +cli.commands['run'].params.append( + click.Option( + ('--pit-tax', cli_prefix + "pit_tax"), + is_flag=True, default=False, + help="[sys_transaction_cost] use historical tax" + ) +) + def load_mod(): from .mod import TransactionCostMod diff --git a/rqalpha/mod/rqalpha_mod_sys_transaction_cost/deciders.py b/rqalpha/mod/rqalpha_mod_sys_transaction_cost/deciders.py index f394b899b..cedf75a9f 100644 --- a/rqalpha/mod/rqalpha_mod_sys_transaction_cost/deciders.py +++ b/rqalpha/mod/rqalpha_mod_sys_transaction_cost/deciders.py @@ -13,10 +13,15 @@ # 详细的授权流程,请联系 public@ricequant.com 获取。 from collections import defaultdict +from datetime import datetime from rqalpha.interface import AbstractTransactionCostDecider from rqalpha.environment import Environment from rqalpha.const import SIDE, HEDGE_TYPE, COMMISSION_TYPE, POSITION_EFFECT +from rqalpha.core.events import EVENT + + +STOCK_PIT_TAX_CHANGE_DATE = datetime(2023, 8, 28) class StockTransactionCostDecider(AbstractTransactionCostDecider): @@ -75,9 +80,11 @@ def get_order_transaction_cost(self, order): class CNStockTransactionCostDecider(StockTransactionCostDecider): - def __init__(self, commission_multiplier, min_commission, tax_multiplier): + def __init__(self, commission_multiplier, min_commission, tax_multiplier, pit_tax, event_bus): super(CNStockTransactionCostDecider, self).__init__(0.0008, commission_multiplier, min_commission) self.tax_rate = 0.0005 + if pit_tax: + event_bus.add_listener(EVENT.PRE_BEFORE_TRADING, self.set_tax_rate) self.tax_multiplier = tax_multiplier def _get_tax(self, order_book_id, side, cost_money): @@ -85,6 +92,12 @@ def _get_tax(self, order_book_id, side, cost_money): if instrument.type != 'CS': return 0 return cost_money * self.tax_rate * self.tax_multiplier if side == SIDE.SELL else 0 + + def set_tax_rate(self, event): + if event.trading_dt < STOCK_PIT_TAX_CHANGE_DATE: + self.tax_rate = 0.001 + else: + self.tax_rate = 0.0005 class CNFutureTransactionCostDecider(AbstractTransactionCostDecider): @@ -94,30 +107,30 @@ def __init__(self, commission_multiplier): self.env = Environment.get_instance() - def _get_commission(self, order_book_id, position_effect, price, quantity, close_today_quantity): - info = self.env.data_proxy.get_commission_info(order_book_id) + def _get_commission(self, order_book_id, position_effect, price, quantity, close_today_quantity, dt): + # type: (str, POSITION_EFFECT, float, int, int, datetime.date) -> float + info = self.env.data_proxy.get_futures_trading_parameters(order_book_id, dt) commission = 0 - if info['commission_type'] == COMMISSION_TYPE.BY_MONEY: + if info.commission_type == COMMISSION_TYPE.BY_MONEY: contract_multiplier = self.env.get_instrument(order_book_id).contract_multiplier if position_effect == POSITION_EFFECT.OPEN: - commission += price * quantity * contract_multiplier * info[ - 'open_commission_ratio'] + commission += price * quantity * contract_multiplier * info.open_commission_ratio else: commission += price * ( quantity - close_today_quantity - ) * contract_multiplier * info['close_commission_ratio'] - commission += price * close_today_quantity * contract_multiplier * info['close_commission_today_ratio'] + ) * contract_multiplier * info.close_commission_ratio + commission += price * close_today_quantity * contract_multiplier * info.close_commission_today_ratio else: if position_effect == POSITION_EFFECT.OPEN: - commission += quantity * info['open_commission_ratio'] + commission += quantity * info.open_commission_ratio else: - commission += (quantity - close_today_quantity) * info['close_commission_ratio'] - commission += close_today_quantity * info['close_commission_today_ratio'] + commission += (quantity - close_today_quantity) * info.close_commission_ratio + commission += close_today_quantity * info.close_commission_today_ratio return commission * self.commission_multiplier def get_trade_commission(self, trade): return self._get_commission( - trade.order_book_id, trade.position_effect, trade.last_price, trade.last_quantity, trade.close_today_amount + trade.order_book_id, trade.position_effect, trade.last_price, trade.last_quantity, trade.close_today_amount, trade.trading_datetime.date() ) def get_trade_tax(self, trade): @@ -127,5 +140,5 @@ def get_order_transaction_cost(self, order): close_today_quantity = order.quantity if order.position_effect == POSITION_EFFECT.CLOSE_TODAY else 0 return self._get_commission( - order.order_book_id, order.position_effect, order.frozen_price, order.quantity, close_today_quantity + order.order_book_id, order.position_effect, order.frozen_price, order.quantity, close_today_quantity, order.trading_datetime.date() ) diff --git a/rqalpha/mod/rqalpha_mod_sys_transaction_cost/mod.py b/rqalpha/mod/rqalpha_mod_sys_transaction_cost/mod.py index 5cc56e1c4..a4ffb9bf1 100644 --- a/rqalpha/mod/rqalpha_mod_sys_transaction_cost/mod.py +++ b/rqalpha/mod/rqalpha_mod_sys_transaction_cost/mod.py @@ -47,7 +47,7 @@ def start_up(self, env, mod_config): continue env.set_transaction_cost_decider(instrument_type, CNStockTransactionCostDecider( stock_commission_multiplier, mod_config.cn_stock_min_commission, - mod_config.tax_multiplier + mod_config.tax_multiplier, mod_config.pit_tax, env.event_bus )) env.set_transaction_cost_decider(INSTRUMENT_TYPE.FUTURE, CNFutureTransactionCostDecider( diff --git a/rqalpha/model/instrument.py b/rqalpha/model/instrument.py index eb08e43e2..fde4a65c0 100644 --- a/rqalpha/model/instrument.py +++ b/rqalpha/model/instrument.py @@ -18,7 +18,9 @@ import re import copy import datetime +import inspect from typing import Dict, Callable, Optional +from methodtools import lru_cache import numpy as np from dateutil.parser import parse @@ -46,10 +48,12 @@ def _fix_date(ds, dflt=None) -> datetime: __repr__ = property_repr - def __init__(self, dic, future_tick_size_getter=None): - # type: (Dict, Optional[Callable[[Instrument], float]]) -> None + def __init__(self, dic, futures_tick_size_getter=None, futures_long_margin_ratio_getter=None, futures_short_margin_ratio_getter=None): + # type: (Dict, Optional[Callable[[Instrument], float]], Optional[Callable[[Instrument], float]], Optional[Callable[[Instrument], float]]) -> None self.__dict__ = copy.copy(dic) - self._future_tick_size_getter = future_tick_size_getter + self._futures_tick_size_getter = futures_tick_size_getter + self._futures_long_margin_ratio_getter = futures_long_margin_ratio_getter + self._futures_short_margin_ratio_getter = futures_short_margin_ratio_getter if "listed_date" in dic: self.__dict__["listed_date"] = self._fix_date(dic["listed_date"]) @@ -235,13 +239,6 @@ def contract_multiplier(self): """ return self.__dict__.get('contract_multiplier', 1) - @property - def margin_rate(self): - """ - [float] 合约最低保证金率(期货专用) - """ - return self.__dict__.get("margin_rate", 1) - @property def underlying_order_book_id(self): """ @@ -444,17 +441,37 @@ def tick_size(self): elif self.type in ("ETF", "LOF"): return 0.001 elif self.type == INSTRUMENT_TYPE.FUTURE: - return self._future_tick_size_getter(self) + return self._futures_tick_size_getter(self) else: raise NotImplementedError - def calc_cash_occupation(self, price, quantity, direction): - # type: (float, float, POSITION_DIRECTION) -> float + @lru_cache(8) + def get_long_margin_ratio(self, dt): + # type: (datetime.date) -> float + """ + 获取多头保证金率(期货专用) + """ + return self._futures_long_margin_ratio_getter(self, dt) + + @lru_cache(8) + def get_short_margin_ratio(self, dt): + # type: (datetime.date) -> float + """ + 获取空头保证金率(期货专用) + """ + return self._futures_long_margin_ratio_getter(self, dt) + + def calc_cash_occupation(self, price, quantity, direction, dt): + # type: (float, int, POSITION_DIRECTION, datetime.date) -> float if self.type in INST_TYPE_IN_STOCK_ACCOUNT: return price * quantity elif self.type == INSTRUMENT_TYPE.FUTURE: margin_multiplier = Environment.get_instance().config.base.margin_multiplier - return price * quantity * self.contract_multiplier * self.margin_rate * margin_multiplier + if direction == POSITION_DIRECTION.LONG: + margin_rate = self.get_long_margin_ratio(dt) + elif direction == POSITION_DIRECTION.SHORT: + margin_rate = self.get_short_margin_ratio(dt) + return price * quantity * self.contract_multiplier * margin_rate * margin_multiplier else: raise NotImplementedError diff --git a/rqalpha/portfolio/account.py b/rqalpha/portfolio/account.py index d240d0317..4df32743c 100644 --- a/rqalpha/portfolio/account.py +++ b/rqalpha/portfolio/account.py @@ -30,6 +30,7 @@ from rqalpha.utils.i18n import gettext as _ from rqalpha.utils.logger import user_system_log from rqalpha.portfolio.position import Position, PositionProxyDict +from rqalpha.mod.rqalpha_mod_sys_accounts.position_model import FuturePosition OrderApiType = Callable[[str, Union[int, float], OrderStyle, bool], List[Order]] @@ -105,6 +106,7 @@ def register_event(self): event_bus.add_listener(EVENT.PRE_BEFORE_TRADING, self._on_before_trading) event_bus.add_listener(EVENT.SETTLEMENT, self._on_settlement) + event_bus.add_listener(EVENT.POST_SETTLEMENT, self._post_settlement) event_bus.prepend_listener(EVENT.BAR, self._on_bar) event_bus.prepend_listener(EVENT.TICK, self._on_tick) @@ -350,6 +352,16 @@ def _on_settlement(self, event): user_system_log.warn(_("Trigger Forced Liquidation, current total_value is 0")) self._positions.clear() self._total_cash = 0 + + def _post_settlement(self, event): + # type: (EVENT) -> None + """ + 该事件必须在 post_settlement 中最后执行,若有其他事件要加入到 post_settlement 中,请使用 event_bus.prepend_listener 添加 + """ + for order_book_id, positions in list(self._positions.items()): + for position in six.itervalues(positions): + if isinstance(position, FuturePosition): + position.post_settlement() def _on_order_pending_new(self, event): if event.account != self: @@ -443,7 +455,7 @@ def _on_bar(self, _): def _frozen_cash_of_order(self, order): if order.position_effect == POSITION_EFFECT.OPEN: instrument = self._env.data_proxy.instrument(order.order_book_id) - order_cost = instrument.calc_cash_occupation(order.frozen_price, order.quantity, order.position_direction) + order_cost = instrument.calc_cash_occupation(order.frozen_price, order.quantity, order.position_direction, order.trading_datetime.date()) else: order_cost = 0 return order_cost + self._env.get_order_transaction_cost(order) diff --git a/rqalpha/utils/exception.py b/rqalpha/utils/exception.py index d4b6dfb15..a80c6d3c0 100644 --- a/rqalpha/utils/exception.py +++ b/rqalpha/utils/exception.py @@ -131,3 +131,7 @@ class RQTypeError(RQUserError): class RQApiNotSupportedError(RQUserError): pass + +class RQDatacVersionTooLow(RuntimeError): + pass + diff --git a/rqalpha/utils/repr.py b/rqalpha/utils/repr.py index e26cee153..6ebfd236e 100644 --- a/rqalpha/utils/repr.py +++ b/rqalpha/utils/repr.py @@ -36,7 +36,7 @@ def __new__(mcs, *args, **kwargs): else: repr_properties = [] for c in cls.mro(): - repr_properties.extend(v for v in vars(c) if isinstance(getattr(c, v), property)) + repr_properties.extend(v for v in list(vars(c).keys()) if isinstance(getattr(c, v), property)) cls.__repr__ = _repr(cls.__name__, repr_properties) return cls diff --git a/rqalpha/utils/translations/zh_Hans_CN/LC_MESSAGES/messages.mo b/rqalpha/utils/translations/zh_Hans_CN/LC_MESSAGES/messages.mo index 0426d7f0e..54398e717 100644 Binary files a/rqalpha/utils/translations/zh_Hans_CN/LC_MESSAGES/messages.mo and b/rqalpha/utils/translations/zh_Hans_CN/LC_MESSAGES/messages.mo differ diff --git a/rqalpha/utils/translations/zh_Hans_CN/LC_MESSAGES/messages.po b/rqalpha/utils/translations/zh_Hans_CN/LC_MESSAGES/messages.po index af76df604..39dfd419c 100644 --- a/rqalpha/utils/translations/zh_Hans_CN/LC_MESSAGES/messages.po +++ b/rqalpha/utils/translations/zh_Hans_CN/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2022-06-24 10:33+0800\n" +"POT-Creation-Date: 2024-01-19 17:56+0800\n" "PO-Revision-Date: 2016-10-24 21:20+0800\n" "Last-Translator: FULL NAME \n" "Language: zh_Hans_CN\n" @@ -16,7 +16,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.10.1\n" +"Generated-By: Babel 2.13.1\n" #: rqalpha/environment.py:70 msgid "" @@ -28,13 +28,13 @@ msgstr "Environment 并没有被创建,请再 RQAlpha 初始化之后使用 `E msgid "No such transaction cost decider, order_book_id = {}" msgstr "" -#: rqalpha/main.py:61 +#: rqalpha/main.py:60 msgid "" "There is no data between {start_date} and {end_date}. Please check your " "data bundle or select other backtest period." msgstr "未在 {start_date} 和 {end_date} 区间内查询到数据,请检查并升级您的 data bundle 或者选择其他回测区间。" -#: rqalpha/main.py:80 +#: rqalpha/main.py:79 msgid "" "Missing persist provider. You need to set persist_provider before use " "persist" @@ -46,95 +46,91 @@ msgstr "" "rqdatac 初始化失败,部分扩展 API 可能无法使用,错误信息:{}。您可以访问 " "https://www.ricequant.com/welcome/rqdata 获取 rqdatac" -#: rqalpha/main.py:213 +#: rqalpha/main.py:219 msgid "system restored" msgstr "" -#: rqalpha/main.py:243 +#: rqalpha/main.py:249 msgid "strategy run successfully, normal exit" msgstr "策略运行成功,正常退出" -#: rqalpha/main.py:248 +#: rqalpha/main.py:254 msgid "strategy execute exception" msgstr "策略运行产生异常" -#: rqalpha/apis/api_base.py:67 rqalpha/apis/api_base.py:266 -#: rqalpha/apis/api_base.py:302 +#: rqalpha/apis/api_base.py:69 rqalpha/apis/api_base.py:286 +#: rqalpha/apis/api_base.py:322 msgid "unsupported order_book_id type" msgstr "不支持该 order_book_id 类型" -#: rqalpha/apis/api_base.py:87 rqalpha/apis/api_base.py:91 -msgid "Limit order price should not be nan." -msgstr "限价单价格不能为 nan。" - -#: rqalpha/apis/api_base.py:148 +#: rqalpha/apis/api_base.py:164 #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:59 msgid "Main Future contracts[88] are not supported in paper trading." msgstr "期货主力连续合约[88] 在实盘模拟中暂不支持。" -#: rqalpha/apis/api_base.py:152 +#: rqalpha/apis/api_base.py:168 #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:61 msgid "Index Future contracts[99] are not supported in paper trading." msgstr "期货指数连续合约[99] 在实盘模拟中暂不支持。" -#: rqalpha/apis/api_base.py:158 +#: rqalpha/apis/api_base.py:174 #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:66 -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:89 -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:128 -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:308 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:104 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:142 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:346 msgid "Order Creation Failed: [{order_book_id}] No market data" msgstr "订单创建失败: 当前合约[{order_book_id}]没有市场数据。" -#: rqalpha/apis/api_rqdatac.py:46 +#: rqalpha/apis/api_rqdatac.py:50 msgid "rqdatac is not available, extension apis will not function properly" msgstr "" "rqdatac 不存在,扩展 API 无法使用,您可以访问 https://www.ricequant.com/welcome/rqdata 获取" " rqdatac" -#: rqalpha/apis/api_rqdatac.py:102 +#: rqalpha/apis/api_rqdatac.py:106 msgid "in get_split, start_date {} is no earlier than the previous test day {}" msgstr "get_spit 中 start_date {} 晚于策略运行前一日 {}" -#: rqalpha/apis/api_rqdatac.py:140 +#: rqalpha/apis/api_rqdatac.py:144 msgid "in index_components, date {} is no earlier than test date {}" msgstr "index_compoments 中,date {} 晚于策略当前运行日期 {}" -#: rqalpha/apis/api_rqdatac.py:187 +#: rqalpha/apis/api_rqdatac.py:191 msgid "in index_weights, date {} is no earlier than previous test date {}" msgstr "在API index_weights 中, 参数date={} 不应晚于上一交易日 {}" -#: rqalpha/apis/api_rqdatac.py:330 +#: rqalpha/apis/api_rqdatac.py:398 msgid "in get_price, end_date {} is no earlier than the previous test day {}" msgstr "在API get_price 中, 当前参数end_date={} 不应晚于上一交易日 {}" -#: rqalpha/apis/api_rqdatac.py:338 +#: rqalpha/apis/api_rqdatac.py:406 msgid "in get_price, start_date {} is no earlier than the previous test day {}" msgstr "在API get_price中, 参数start_date={} 不应晚于上一交易日 {}" -#: rqalpha/apis/api_rqdatac.py:343 +#: rqalpha/apis/api_rqdatac.py:411 msgid "in get_price, start_date {} > end_date {}" msgstr "在API get_price 中,当前参数 start_date={} 大于 end_date={}" -#: rqalpha/apis/api_rqdatac.py:805 +#: rqalpha/apis/api_rqdatac.py:873 msgid "'{0}' future does not exist" msgstr "期货品种 '{0}' 不存在" -#: rqalpha/apis/api_rqdatac.py:962 +#: rqalpha/apis/api_rqdatac.py:1159 msgid "in get_fundamentals entry_date {} is no earlier than test date {}" msgstr "在API get_fundamentals 中,财务数据开始日期 {} 不应晚于上一交易日 {}" -#: rqalpha/apis/api_rqdatac.py:996 rqalpha/apis/api_rqdatac.py:1061 -#: rqalpha/utils/arg_checker.py:311 +#: rqalpha/apis/api_rqdatac.py:1193 rqalpha/apis/api_rqdatac.py:1258 +#: rqalpha/utils/arg_checker.py:320 msgid "" "function {}: invalid {} argument, quarter should be in form of '2012q3', " "got {} (type: {})" msgstr "函数 {}:参数 {} 不合法,季度应为形如 '2012q3' 的字符串,传入的是 {}(类型:{})" -#: rqalpha/cmds/bundle.py:33 +#: rqalpha/cmds/bundle.py:34 msgid "create bundle using RQDatac" msgstr "使用 RQDatac 创建 Bundle" -#: rqalpha/cmds/bundle.py:43 +#: rqalpha/cmds/bundle.py:44 msgid "" "rqdatac is required to create bundle. you can visit " "https://www.ricequant.com/welcome/rqdata to get rqdatac, or use \"rqalpha" @@ -143,15 +139,15 @@ msgstr "" "创建 bundle 依赖 rqdatac。您可以访问 https://www.ricequant.com/welcome/rqdata 以获取 " "rqdatac,或执行 \"rqalpha download-bundle\" 以下载月度更新的 bundle。" -#: rqalpha/cmds/bundle.py:54 rqalpha/cmds/bundle.py:83 +#: rqalpha/cmds/bundle.py:55 rqalpha/cmds/bundle.py:84 msgid "rqdatac init failed with error: {}" msgstr "rqdatac 初始化失败:{}" -#: rqalpha/cmds/bundle.py:62 +#: rqalpha/cmds/bundle.py:63 msgid "Update bundle using RQDatac" msgstr "使用 RQDatac 更新 Bundle" -#: rqalpha/cmds/bundle.py:72 +#: rqalpha/cmds/bundle.py:73 msgid "" "rqdatac is required to update bundle. you can visit " "https://www.ricequant.com/welcome/rqdata to get rqdatac, or use \"rqalpha" @@ -160,45 +156,15 @@ msgstr "" "更新 bundle 依赖 rqdatac。您可以访问 https://www.ricequant.com/welcome/rqdata 以获取 " "rqdatac,或执行 \"rqalpha download-bundle\" 以下载月度更新的 bundle。" -#: rqalpha/cmds/bundle.py:87 +#: rqalpha/cmds/bundle.py:88 msgid "bundle not exist, use \"rqalpha create-bundle\" command instead" msgstr "bundle 不存在,执行 \"rqalpha create-bundle\" 以创建 bundle" -#: rqalpha/cmds/bundle.py:94 +#: rqalpha/cmds/bundle.py:95 msgid "Download bundle (monthly updated)" msgstr "下载(月度更新的)Bundle" -msgid "Check bundle" -msgstr "检测 Bundle 数据" - -msgid "corrupted files" -msgstr "已损坏的文件" - -msgid "remove files" -msgstr "是否删除文件" - -msgid "remove success" -msgstr "文件删除完毕" - -msgid "corrupted files not remove" -msgstr "已损坏的文件尚未删除" - -msgid "input error" -msgstr "输入异常" - -msgid "bundle's day bar is incomplete, please update bundle" -msgstr "Bundle日线数据不完整,请及时更新bundle" - -msgid "good bundle's day bar" -msgstr "Bundle日线数据良好" - -msgid "benchmark {} not exists, please entry correct order_book_id" -msgstr "基准标的({})信息不存在,请输入正确的标的代码" - -msgid "benchmark {} listed date {} > backtest start date {} or de_listed date {} <= backtest end date {}" -msgstr "基准标的({})上市日期({})晚于回测起始日期({})或退市日期({})早于回测结束日期({})" - -#: rqalpha/cmds/bundle.py:105 +#: rqalpha/cmds/bundle.py:106 msgid "" "\n" " [WARNING]\n" @@ -212,19 +178,23 @@ msgstr "" " 该路径下的内容将会被移除.\n" " 确定继续吗?" -#: rqalpha/cmds/bundle.py:123 +#: rqalpha/cmds/bundle.py:124 msgid "Data bundle download successfully in {bundle_path}" msgstr "数据下载成功: {bundle_path}" -#: rqalpha/cmds/bundle.py:134 +#: rqalpha/cmds/bundle.py:127 +msgid "Check bundle" +msgstr "检测 Bundle 数据" + +#: rqalpha/cmds/bundle.py:141 msgid "try {} ..." msgstr "尝试 {} ..." -#: rqalpha/cmds/bundle.py:146 +#: rqalpha/cmds/bundle.py:153 msgid "downloading ..." msgstr "下载中 ..." -#: rqalpha/cmds/bundle.py:160 +#: rqalpha/cmds/bundle.py:167 msgid "" "\n" "Download failed, retry in {} seconds." @@ -232,6 +202,34 @@ msgstr "" "\n" "下载失败,{} 秒后重试。" +#: rqalpha/cmds/bundle.py:188 +msgid "corrupted files" +msgstr "已损坏的文件" + +#: rqalpha/cmds/bundle.py:189 +msgid "remove files" +msgstr "是否删除文件" + +#: rqalpha/cmds/bundle.py:192 +msgid "remove success" +msgstr "文件删除完毕" + +#: rqalpha/cmds/bundle.py:194 +msgid "corrupted files not remove" +msgstr "已损坏的文件尚未删除" + +#: rqalpha/cmds/bundle.py:196 +msgid "input error" +msgstr "输入异常" + +#: rqalpha/cmds/bundle.py:198 +msgid "bundle's day bar is incomplete, please update bundle" +msgstr "Bundle日线数据不完整,请及时更新bundle" + +#: rqalpha/cmds/bundle.py:200 +msgid "good bundle's day bar" +msgstr "Bundle日线数据良好" + #: rqalpha/cmds/misc.py:26 msgid "Generate example strategies to target folder" msgstr "在目标文件夹生成样例策略" @@ -265,12 +263,54 @@ msgstr "策略暂停中,取消执行 {}({}, {})" msgid "deprecated parameter[bar_dict] in before_trading function." msgstr "[Deprecated]在before_trading函数中,第二个参数bar_dict已经不再使用了。" -#: rqalpha/data/base_data_source/storages.py:85 +#: rqalpha/data/bundle.py:454 +msgid "" +"RQAlpha already supports backtesting using futures historical margins and" +" rates, please upgrade RQDatac to version 2.11.12 and above to use it" +msgstr "RQAlpha 已支持使用期货历史保证金和费率进行回测,请将 RQDatac 升级至 2.11.12 及以上版本进行使用" + +#: rqalpha/data/bundle.py:464 rqalpha/data/bundle.py:522 +msgid "" +"Futures historical trading parameters data is being updated, please " +"wait......" +msgstr "正在更新期货历史交易参数,请稍后......" + +#: rqalpha/data/bundle.py:516 +msgid "" +"File {} update failed, if it is using, please update later, or you can " +"delete then update again" +msgstr "{} 文件更新失败,如果其正在使用中,请稍后再进行更新,或者您可以将其删除后再重新更新" + +#: rqalpha/data/base_data_source/data_source.py:140 +msgid "" +"RQDatac is not installed, " +"\"config.base.futures_time_series_trading_parameters\" will be disabled." +msgstr "未安装 RQDatac,将关闭配置 \"config.base.futures_time_series_trading_parameters\"" + +#: rqalpha/data/base_data_source/data_source.py:145 +msgid "" +"RQDatac does not have permission to obtain futures histrical trading " +"parameters, \"config.base.futures_time_series_trading_parameters\" will " +"be disabled." +msgstr "" +"RQDatac 缺少获取期货历史交易参数的权限,将关闭配置 " +"\"config.base.futures_time_series_trading_parameters\"" + +#: rqalpha/data/base_data_source/storages.py:81 +msgid "" +"Your bundle data is too old, please use 'rqalpha update-bundle' or " +"'rqalpha download-bundle' to update it to lastest before using" +msgstr "" +"您的 Bundle 数据过旧,请使用 'rqalpha update-bundle' 或 'rqalpha download-bundle' " +"将其更新至最新后再进行使用" + +#: rqalpha/data/base_data_source/storages.py:97 +#: rqalpha/data/base_data_source/storages.py:123 msgid "unsupported future instrument {}" msgstr "不支持的期货标的{}" -#: rqalpha/data/base_data_source/storages.py:155 -#: rqalpha/data/base_data_source/storages.py:165 +#: rqalpha/data/base_data_source/storages.py:194 +#: rqalpha/data/base_data_source/storages.py:204 msgid "" "open data bundle failed, you can remove {} and try to regenerate bundle: " "{}" @@ -298,7 +338,7 @@ msgstr "" msgid "mod tear_down [END] {}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/position_model.py:296 +#: rqalpha/mod/rqalpha_mod_sys_accounts/position_model.py:314 msgid "{order_book_id} is expired, close all positions by system" msgstr "{order_book_id} 已退市/交割,系统自动平仓" @@ -316,18 +356,16 @@ msgid "" "{closable}" msgstr "订单创建失败:{order_book_id} 的仓位不足,目标平仓/行权量为 {quantity},可平仓/行权量为 {closable}" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:49 -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:96 -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:144 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:50 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:112 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:158 msgid "Order Creation Failed: 0 order quantity, order_book_id={order_book_id}" msgstr "{order_book_id} 的订单创建失败:下单数量为 0" -# msgid "Order Creation Failed: Order amount is 0." -# msgstr "订单创建失败: 下单量为 0" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:54 -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:85 -msgid "Limit order price should be positive" -msgstr "Limit order 价格应该为正,请检查您的下单价格" +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:55 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:100 +msgid "Limit order price should not be nan." +msgstr "限价单价格不能为 nan。" #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:78 msgid "" @@ -347,17 +385,17 @@ msgid "" "[{new_orders_repr}]" msgstr "订单被拆分,原订单:{original_order_repr},新订单:[{new_orders_repr}]" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:65 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:66 msgid "" "order_book_id: {order_book_id} needs stock account, please set and try " "again!" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:106 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:120 msgid "insufficient cash, use all remaining cash({}) to create order" msgstr "现金不足,使用当前剩余资金({})创建订单" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:296 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:334 #, fuzzy msgid "" "function order_target_portfolio: invalid keys of target_portfolio, " @@ -366,285 +404,323 @@ msgstr "" "函数 order_target_portfolio:不合法的参数。target_portfolio 的键应为合约代码或 Instrument " "对象,实际传入了 {}(类型:{})。" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:301 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:339 msgid "" "function order_target_portfolio: invalid instrument type, excepted " "CS/ETF/LOF/INDX, got {}" msgstr "函数 order_target_portfolio:不合法的资产类型。应传入股票、ETF、LOF 或指数,实际传入了 {}。" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:318 -#, fuzzy -msgid "" -"function order_target_portfolio: invalid order price {target_price} of " -"{id_or_ins}" -msgstr "函数 order_target_portfolio:{id_or_ins} 的下单价格 {target_price} 不合法。" - -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:324 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:354 #, fuzzy msgid "" "function order_target_portfolio: invalid values of target_portfolio, " "excepted float between 0 and 1, got {} (key: {})" msgstr "函数 order_target_portfolio:不合法的目标权重,应传入 0 和 1 之间的浮点数,实际传入了 {}(类型:{})。" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:332 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:363 msgid "total percent should be lower than 1, current: {}" msgstr "目标持仓占比总和应小于 1,当前值:{}。" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:655 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:383 +msgid "" +"Adjust position of {id_or_ins} Failed: Invalid close/open price " +"{close_price}/{open_price}" +msgstr "" + +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:698 msgid "in get_dividend, start_date {} is later than the previous test day {}" msgstr "在 get_dividend 函数中,start_date {} 晚于当前回测时间的上一个交易日 {}。" -#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:64 +#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:66 msgid "[sys_analyser] save report" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:69 +#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:71 msgid "[sys_analyser] output result pickle file" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:74 +#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:76 msgid "[sys_analyser] plot result" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:79 +#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:81 msgid "[sys_analyser] save plot to file" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:84 +#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:86 msgid "[sys_analyser] order_book_id of benchmark" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:89 +#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:91 msgid "[sys_analyser] show open close points on plot" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:94 +#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:96 msgid "[sys_analyser] show weekly indicators and return curve on plot" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:98 +#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:100 msgid "[sys_analyser] Plot from strategy output file" msgstr "【sys_analyser】使用策略输出的文件绘制收益图" -#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:101 +#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:103 msgid "save plot result to file" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:102 +#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:104 msgid "show open close points on plot" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:103 +#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:105 msgid "show weekly indicators and return curve on plot" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:113 +#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:115 msgid "[sys_analyser] Generate report from strategy output file" msgstr "【sys_analyser】使用策略输出的文件生成报告" -#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:108 +#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:112 msgid "" "config 'base.benchmark' is deprecated, use 'mod.sys_analyser.benchmark' " "instead" msgstr "配置'base.benchmark'已被弃用,使用'mod.sys_analyser.benchmark'代替" -#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:190 +#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:143 +msgid "benchmark {} not exists, please entry correct order_book_id" +msgstr "基准标的({})信息不存在,请输入正确的标的代码" + +#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:147 +msgid "" +"benchmark {} listed date {} > backtest start date {} or de_listed date {}" +" <= backtest end date {}" +msgstr "基准标的({})上市日期({})晚于回测起始日期({})或退市日期({})早于回测结束日期({})" + +#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:210 msgid "invalid init benchmark {}, should be in format 'order_book_id:weight'" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:195 +#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:215 msgid "invalid weight for instrument {order_book_id}: {weight}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:61 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:60 msgid "Strategy" msgstr "策略收益" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:62 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:61 msgid "Benchmark" msgstr "基准收益" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:63 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:62 msgid "Excess" msgstr "超额收益" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:64 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:63 msgid "Weekly" msgstr "策略周度收益" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:65 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:64 msgid "BenchmarkWeekly" msgstr "基准周度收益" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:67 -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:91 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:66 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:123 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:170 msgid "MaxDrawDown" msgstr "最大回撤" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:68 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:67 msgid "MaxDDD" msgstr "最长回撤持续期" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:69 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:68 msgid "Open" msgstr "开仓" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:70 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:69 msgid "Close" msgstr "平仓" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:73 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:103 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:155 msgid "TotalReturns" msgstr "策略收益率" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:74 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:104 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:156 msgid "AnnualReturns" msgstr "策略年化收益率" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:75 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:105 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:157 msgid "Alpha" msgstr "阿尔法" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:76 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:106 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:158 msgid "Beta" msgstr "贝塔" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:77 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:107 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:161 msgid "Sharpe" msgstr "夏普率" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:78 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:108 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:165 msgid "Sortino" msgstr "索提诺比率" +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:109 msgid "WeeklyUlcerIndex" msgstr "周度累计回撤深度" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:80 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:111 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:175 msgid "BenchmarkReturns" msgstr "基准收益率" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:81 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:112 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:176 msgid "BenchmarkAnnual" msgstr "基准年化收益率" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:82 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:113 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:167 msgid "Volatility" msgstr "波动率" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:83 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:114 msgid "TrackingError" msgstr "跟踪误差" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:84 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:115 msgid "DownsideRisk" msgstr "下行风险" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:85 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:116 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:164 msgid "InformationRatio" msgstr "信息比率" +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:117 msgid "WeeklyUlcerPerformanceIndex" msgstr "周度累计回撤夏普率" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:87 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:119 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:179 msgid "ExcessCumReturns" msgstr "超额收益率(算术)" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:88 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:120 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:159 msgid "WinRate" msgstr "胜率" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:89 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:121 msgid "WeeklyWinRate" msgstr "周度胜率" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:90 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:122 msgid "ProfitLossRate" msgstr "盈亏比" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:92 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:124 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:171 msgid "MaxDD/MaxDDD" msgstr "最大回撤/最长回撤持续期" +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:125 msgid "WeeklyExcessUlcerIndex" msgstr "周度超额累计回撤深度" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:96 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:129 msgid "WeeklyAlpha" msgstr "周度阿尔法" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:97 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:130 msgid "WeeklyBeta" msgstr "周度贝塔" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:98 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:131 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:162 msgid "WeeklySharpe" msgstr "周度夏普率" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:99 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:132 msgid "WeeklyInfoRatio" msgstr "周度信息比率" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:100 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:133 msgid "WeeklyTrackingError" msgstr "周度跟踪误差" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:101 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:134 msgid "WeeklyMaxDrawdown" msgstr "周度最大回撤" -msgid "WeeklyVolatility" -msgstr "周度波动率" - -msgid "MonthlySharpe" -msgstr "月度夏普率" - -msgid "MonthlyVolatility" -msgstr "月度波动率" - -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:105 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:138 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:177 msgid "ExcessReturns" msgstr "超额收益率(几何)" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:106 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:139 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:178 msgid "ExcessAnnual" msgstr "年化超额收益率(几何)" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:107 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:140 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:181 msgid "ExcessSharpe" msgstr "超额夏普率" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:108 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:141 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:182 msgid "ExcessVolatility" msgstr "超额收益波动率" -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:109 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:142 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:184 msgid "ExcessMaxDD" msgstr "超额收益最大回撤(几何)" -msgid "ExcessWinRate" -msgstr "超额胜率" - -#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:110 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:143 +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:185 msgid "ExcessMaxDD/ExcessMaxDDD" msgstr "超额收益最大回撤/最长回撤持续期" +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:144 msgid "WeeklyExcessUlcerPerformanceIndex" msgstr "周度超额累计回撤夏普率" +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:163 +msgid "MonthlySharpe" +msgstr "月度夏普率" + +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:168 +msgid "WeeklyVolatility" +msgstr "周度波动率" + +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:169 +msgid "MonthlyVolatility" +msgstr "月度波动率" + +#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:183 +msgid "ExcessWinRate" +msgstr "超额胜率" + #: rqalpha/mod/rqalpha_mod_sys_risk/validators/cash_validator.py:33 msgid "" "Order Creation Failed: not enough money to buy {order_book_id}, needs " "{cost_money:.2f}, cash {cash:.2f}" msgstr "订单创建失败: 可用资金不足。当前资金: {cash:.2f},{order_book_id} 下单所需资金: {cost_money:.2f}" -#: rqalpha/mod/rqalpha_mod_sys_risk/validators/is_trading_validator.py:31 +#: rqalpha/mod/rqalpha_mod_sys_risk/validators/is_trading_validator.py:32 #, fuzzy msgid "Order Creation Failed: {order_book_id} is not listing!" msgstr "订单创建失败: {order_book_id} 未上市或已退市" -#: rqalpha/mod/rqalpha_mod_sys_risk/validators/is_trading_validator.py:37 +#: rqalpha/mod/rqalpha_mod_sys_risk/validators/is_trading_validator.py:38 msgid "Order Creation Failed: security {order_book_id} is suspended on {date}" msgstr "订单创建失败: {order_book_id} 在 {date} 时停牌" @@ -666,41 +742,39 @@ msgid "" "trade: [{}...]" msgstr "订单创建失败,当前存在可能导致自成交的挂单:[{}...]" -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:101 -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:319 -#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:66 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:118 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:335 +#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:69 msgid "" "Order Cancelled: current security [{order_book_id}] can not be traded in " "listed date [{listed_date}]" msgstr "订单被撤销: [{order_book_id}] 上市首日无法交易" -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:108 -#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:72 -msgid "Order Cancelled: current bar [{order_book_id}] miss market data." -msgstr "订单被拒单: [{order_book_id}] 当前缺失市场数据。" - -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:128 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:125 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:165 #: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:86 +msgid "Order Cancelled: {order_book_id} bar no volume" +msgstr "订单被撤销:{order_book_id} 当前无成交量" + +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:150 +#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:95 msgid "Order Cancelled: current bar [{order_book_id}] reach the limit_up price." msgstr "订单被拒单: [{order_book_id}] 已涨停。" -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:134 -#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:95 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:156 +#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:104 msgid "Order Cancelled: current bar [{order_book_id}] reach the limit_down price." msgstr "订单被拒单: [{order_book_id}] 已跌停。" -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:143 -msgid "Order Cancelled: {order_book_id} bar no volume" -msgstr "订单被撤销:{order_book_id} 当前无成交量" - -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:159 -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:396 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:178 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:417 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:567 msgid "" "Order Cancelled: market order {order_book_id} volume {order_volume} due " "to volume limit" msgstr "订单被撤销: 订单 [{order_book_id}] 的下单量 {order_volume} 超过了成交量限制。" -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:197 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:217 msgid "" "Order Cancelled: market order {order_book_id} volume {order_volume} is " "larger than {volume_percent_limit} percent of current bar volume, fill " @@ -709,26 +783,26 @@ msgstr "" "{order_book_id} 下单量 {order_volume} 超过当前 Bar 成交量的 " "{volume_percent_limit}%,实际成交 {filled_volume}" -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:326 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:343 msgid "Order Cancelled: current tick [{order_book_id}] miss market data." msgstr "订单被拒单: [{order_book_id}] 当前缺失市场数据。" -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:351 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:368 msgid "Order Cancelled: current tick [{order_book_id}] reach the limit_up price." msgstr "订单被拒单: [{order_book_id}] 已涨停。" -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:357 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:374 msgid "" "Order Cancelled: current tick [{order_book_id}] reach the limit_down " "price." msgstr "订单被拒单: [{order_book_id}] 已跌停。" -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:364 -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:370 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:381 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:387 msgid "Order Cancelled: [{order_book_id}] has no liquidity." msgstr "合约 [{order_book_id}] 流动性不足,拒单。" -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:438 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:462 #, fuzzy msgid "" "Order Cancelled: market order {order_book_id} volume {order_volume} is " @@ -738,7 +812,7 @@ msgstr "" "{order_book_id} 下单量 {order_volume} 超过当前 Tick 成交量的 " "{volume_percent_limit}%,实际成交 {filled_volume}" -#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:571 +#: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:611 msgid "" "Order Cancelled: market order {order_book_id} fill {filled_volume} " "actually" @@ -759,14 +833,18 @@ msgid "" "matching_type is 'current_bar'." msgstr "日回测 'next_bar' 撮合方式已废弃,当前按 'current_bar' 撮合方式成交" -#: rqalpha/mod/rqalpha_mod_sys_simulation/mod.py:107 +#: rqalpha/mod/rqalpha_mod_sys_simulation/mod.py:119 msgid "NO account_type = ({}) in {}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:53 +#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:56 msgid "cancel_order function is not supported in signal mode" msgstr "在 Signal 模式下,不支持 cancel_order 函数" +#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:75 +msgid "Order Cancelled: current bar [{order_book_id}] miss market data." +msgstr "订单被拒单: [{order_book_id}] 当前缺失市场数据。" + #: rqalpha/mod/rqalpha_mod_sys_simulation/simulation_broker.py:108 msgid "unsupported position_effect {}" msgstr "不支持的 position_effect {}" @@ -779,7 +857,7 @@ msgstr "订单 {order_id} 被手动取消。" msgid "Order Rejected: {order_book_id} can not match. Market close." msgstr "订单被拒单: {order_book_id} 当天交易结束,订单无法成交。" -#: rqalpha/mod/rqalpha_mod_sys_simulation/simulation_broker.py:184 +#: rqalpha/mod/rqalpha_mod_sys_simulation/simulation_broker.py:185 msgid "{order_book_id} should be subscribed when frequency is tick." msgstr "tick回测下单失败,请使用 subscribe 订阅合约 {order_book_id}。" @@ -807,13 +885,13 @@ msgstr "无效的 滑点 设置: 其值需为正数。" msgid "invalid slippage rate value {} which cause price <= 0" msgstr "无效的 滑点 设置: 该值导致了调整后的价格为负。" -#: rqalpha/mod/rqalpha_mod_sys_transaction_cost/mod.py:30 +#: rqalpha/mod/rqalpha_mod_sys_transaction_cost/mod.py:42 msgid "" "invalid commission multiplier or tax multiplier value: value range is [0," " +∞)" msgstr "" -#: rqalpha/model/bar.py:371 +#: rqalpha/model/bar.py:372 msgid "id_or_symbols {} does not exist" msgstr "您选择的证券[{}]不存在。" @@ -824,117 +902,113 @@ msgid "" " {tax}, {frozen_price}" msgstr "" -#: rqalpha/portfolio/__init__.py:67 +#: rqalpha/portfolio/__init__.py:71 msgid "invalid init position {order_book_id}: no valid price at {date}" msgstr "初始持仓 {order_book_id} 不合法:无法获取到该标 {date} 的价格" -#: rqalpha/portfolio/__init__.py:267 +#: rqalpha/portfolio/__init__.py:278 rqalpha/portfolio/__init__.py:291 msgid "invalid account type {}, choose in {}" msgstr "不合法的账户类型 {}, 请在以下账户类型中选择 {}" -#: rqalpha/portfolio/__init__.py:271 +#: rqalpha/portfolio/__init__.py:282 msgid "Cash add {}. units {} become to {}" msgstr "入金{}元,份额由{}变动为{}" -#: rqalpha/portfolio/account.py:313 +#: rqalpha/portfolio/account.py:352 msgid "Trigger Forced Liquidation, current total_value is 0" msgstr "触发强制清算,当前总权益为 0" -#: rqalpha/portfolio/account.py:457 +#: rqalpha/portfolio/account.py:502 rqalpha/portfolio/account.py:521 msgid "insufficient cash, current {}, target withdrawal {}" msgstr "现金不足,当前现金 {},目标出金 {}" -#: rqalpha/portfolio/position.py:160 -msgid "invalid price of {order_book_id}: {price}" -msgstr "{order_book_id} 的价格不合法:{price}" - -#: rqalpha/utils/arg_checker.py:50 +#: rqalpha/utils/arg_checker.py:51 msgid "" "function {}: invalid {} argument, expect a value of type {}, got {} " "(type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:58 +#: rqalpha/utils/arg_checker.py:59 msgid "valid order_book_id/instrument" msgstr "" -#: rqalpha/utils/arg_checker.py:61 +#: rqalpha/utils/arg_checker.py:62 msgid "valid stock order_book_id/instrument" msgstr "" -#: rqalpha/utils/arg_checker.py:64 +#: rqalpha/utils/arg_checker.py:65 msgid "valid future order_book_id/instrument" msgstr "" -#: rqalpha/utils/arg_checker.py:67 +#: rqalpha/utils/arg_checker.py:68 msgid "listed order_book_id/instrument" msgstr "" -#: rqalpha/utils/arg_checker.py:70 +#: rqalpha/utils/arg_checker.py:71 msgid "function {}: invalid {} argument, expected a {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:96 +#: rqalpha/utils/arg_checker.py:97 msgid "" "function {}: invalid {} argument, expected instrument with types {}, got " "instrument with type {}" msgstr "" -#: rqalpha/utils/arg_checker.py:136 +#: rqalpha/utils/arg_checker.py:137 msgid "function {}: invalid {} argument, expect a number, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:151 +#: rqalpha/utils/arg_checker.py:160 msgid "function {}: invalid {} argument, valid: {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:165 +#: rqalpha/utils/arg_checker.py:174 msgid "function {}: invalid {} argument, valid fields are {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:177 +#: rqalpha/utils/arg_checker.py:186 msgid "function {}: invalid field {}, valid fields are {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:183 rqalpha/utils/arg_checker.py:198 +#: rqalpha/utils/arg_checker.py:192 rqalpha/utils/arg_checker.py:207 msgid "" "function {}: invalid {} argument, expect a string or a list of string, " "got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:225 rqalpha/utils/arg_checker.py:230 +#: rqalpha/utils/arg_checker.py:234 rqalpha/utils/arg_checker.py:239 msgid "function {}: invalid {} argument, expect a valid date, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:241 +#: rqalpha/utils/arg_checker.py:250 msgid "function {}: invalid {} argument, expect a value >= {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:251 +#: rqalpha/utils/arg_checker.py:260 msgid "function {}: invalid {} argument, expect a value > {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:261 +#: rqalpha/utils/arg_checker.py:270 msgid "function {}: invalid {} argument, expect a value <= {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:272 +#: rqalpha/utils/arg_checker.py:281 msgid "function {}: invalid {} argument, expect a value < {}, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:289 +#: rqalpha/utils/arg_checker.py:298 msgid "" "function {}: invalid {} argument, interval should be in form of '1d', " "'3m', '4q', '2y', got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:325 +#: rqalpha/utils/arg_checker.py:334 msgid "" "function {}: invalid {} argument, should be entity like " "Fundamentals.balance_sheet.total_equity, got {} (type: {})" msgstr "" -#: rqalpha/utils/arg_checker.py:344 +#: rqalpha/utils/arg_checker.py:353 #, fuzzy msgid "" "function {}: invalid {} argument, frequency should be in form of '1m', " @@ -1473,3 +1547,26 @@ msgstr "不支持 API {}。请确保您设置了该 API 所需的账户并开启 #~ msgid "Order Creation Failed: {order_book_id} has been delisted!" #~ msgstr "订单创建失败: {order_book_id} 已退市" +# msgid "Order Creation Failed: Order amount is 0." +# msgstr "订单创建失败: 下单量为 0" +#~ msgid "Limit order price should be positive" +#~ msgstr "Limit order 价格应该为正,请检查您的下单价格" + +#~ msgid "" +#~ "function order_target_portfolio: invalid order " +#~ "price {target_price} of {id_or_ins}" +#~ msgstr "函数 order_target_portfolio:{id_or_ins} 的下单价格 {target_price} 不合法。" + +#~ msgid "invalid price of {order_book_id}: {price}" +#~ msgstr "{order_book_id} 的价格不合法:{price}" + +#~ msgid "" +#~ "Your RQData account does not have " +#~ "permission to use futures historical " +#~ "margin and rates, and fixed data " +#~ "will be used for calculations\n" +#~ "You can contact RiceQuant to activate permission: 0755-26569969" +#~ msgstr "" +#~ "您的 RQData 账号没有权限使用期货历史保证金和费率,将使用固定的数据进行回测和计算\n" +#~ "您可联系米筐科技开通相关权限:0755-26569969" + diff --git a/setup.cfg b/setup.cfg index 18281801d..54394e43c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,7 @@ [metadata] name = rqalpha -version = 5.3.4 +version = 5.3.5 [versioneer] VCS = git diff --git a/setup.py b/setup.py index 7d1623c36..4d338f8e3 100644 --- a/setup.py +++ b/setup.py @@ -81,6 +81,8 @@ 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', ], python_requires=">=3.6" ) diff --git a/tests/api_tests/mod/sys_simulation/test_simulation_event_source.py b/tests/api_tests/mod/sys_simulation/test_simulation_event_source.py index a9b05101d..091d081a7 100644 --- a/tests/api_tests/mod/sys_simulation/test_simulation_event_source.py +++ b/tests/api_tests/mod/sys_simulation/test_simulation_event_source.py @@ -18,8 +18,8 @@ __config__ = { "base": { - "start_date": "2015-04-10", - "end_date": "2015-04-20", + "start_date": "2015-04-14", + "end_date": "2015-04-24", "frequency": "1d", "accounts": { "stock": 1000000, diff --git a/tests/api_tests/mod/sys_transaction_cost/test_commission_multiplier.py b/tests/api_tests/mod/sys_transaction_cost/test_commission_multiplier.py index 4855a90aa..71aeac6a9 100644 --- a/tests/api_tests/mod/sys_transaction_cost/test_commission_multiplier.py +++ b/tests/api_tests/mod/sys_transaction_cost/test_commission_multiplier.py @@ -46,10 +46,10 @@ def handle_bar(context, bar_dict): stock_order = order_percent(context.s1, 1) future_order = buy_open(context.s2, 1) env = Environment.get_instance() - future_commission_info = env.data_proxy.get_commission_info(context.s2) + future_commission_info = env.data_proxy.get_futures_trading_parameters(context.s2, bar_dict.dt.date()) context.fixed = False assert stock_order.transaction_cost == 16.66 * 59900 * 8 / 10000 * 2 - assert future_order.transaction_cost == 7308 * 200 * future_commission_info["open_commission_ratio"] * 3 + assert future_order.transaction_cost == 7308 * 200 * future_commission_info.open_commission_ratio * 3 return locals() diff --git a/tests/api_tests/test_config.py b/tests/api_tests/test_config.py index e3e256a7a..2974f7ca6 100644 --- a/tests/api_tests/test_config.py +++ b/tests/api_tests/test_config.py @@ -50,7 +50,7 @@ def assert_almost_equal(first, second): assert round(abs(first - second), 10) == 0 -def test_future_info(): +def test_futures_info(): __config__ = { "base": { "future_info": { diff --git a/tests/outs/test_f_mean_reverting.pkl b/tests/outs/test_f_mean_reverting.pkl index 120ba74fe..b1c1c0923 100644 Binary files a/tests/outs/test_f_mean_reverting.pkl and b/tests/outs/test_f_mean_reverting.pkl differ diff --git a/tests/outs/test_s_pit_tax.pkl b/tests/outs/test_s_pit_tax.pkl new file mode 100644 index 000000000..8247c088d Binary files /dev/null and b/tests/outs/test_s_pit_tax.pkl differ diff --git a/tests/test_f_buy_and_hold.py b/tests/test_f_buy_and_hold.py index 5fd2ec4dc..53de96472 100644 --- a/tests/test_f_buy_and_hold.py +++ b/tests/test_f_buy_and_hold.py @@ -26,6 +26,6 @@ def handle_bar(context, bar_dict): "sys_progress": { "enabled": True, "show": True, - }, + } }, } diff --git a/tests/test_f_mean_reverting.py b/tests/test_f_mean_reverting.py index 5cb40c66a..e6bb1403f 100644 --- a/tests/test_f_mean_reverting.py +++ b/tests/test_f_mean_reverting.py @@ -138,6 +138,6 @@ def handle_bar(context, bar_dict): "sys_progress": { "enabled": True, "show": True, - }, + } }, } diff --git a/tests/test_s_pit_tax.py b/tests/test_s_pit_tax.py new file mode 100644 index 000000000..ddb34e3f2 --- /dev/null +++ b/tests/test_s_pit_tax.py @@ -0,0 +1,42 @@ +def init(context): + context.s1 = "000001.XSHE" + context.count = 0 + + +def before_trading(context): + context.count += 1 + + +def handle_bar(context, bar_dict): + order_shares(context.s1, 1000) + if context.count > 1: + order_shares(context.s1, -1000) + + +def after_trading(context): + pass + + +__config__ = { + "base": { + "start_date": "2023-08-15", + "end_date": "2023-09-10", + "frequency": "1d", + "accounts": { + "stock": 1000000 + }, + }, + "extra": { + "log_level": "error", + "show": True, + }, + "mod": { + "sys_progress": { + "enabled": True, + "show": True, + }, + "sys_transaction_cost": { + "pit_tax": True + } + } +} \ No newline at end of file diff --git a/versioneer.py b/versioneer.py index e389d28c4..645fc868a 100644 --- a/versioneer.py +++ b/versioneer.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -# Version: 0.18 +# Version: 0.28 """The Versioneer - like a rocketeer, but for versions. @@ -7,18 +6,14 @@ ============== * like a rocketeer, but for versions! -* https://github.com/warner/python-versioneer +* https://github.com/python-versioneer/python-versioneer * Brian Warner -* License: Public Domain -* Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6, and pypy -* [![Latest Version] -(https://pypip.in/version/versioneer/badge.svg?style=flat) -](https://pypi.python.org/pypi/versioneer/) -* [![Build Status] -(https://travis-ci.org/warner/python-versioneer.png?branch=master) -](https://travis-ci.org/warner/python-versioneer) - -This is a tool for managing a recorded version number in distutils-based +* License: Public Domain (Unlicense) +* Compatible with: Python 3.7, 3.8, 3.9, 3.10 and pypy3 +* [![Latest Version][pypi-image]][pypi-url] +* [![Build Status][travis-image]][travis-url] + +This is a tool for managing a recorded version number in setuptools-based python projects. The goal is to remove the tedious and error-prone "update the embedded version string" step from your release process. Making a new release should be as easy as recording a new tag in your version-control @@ -27,9 +22,38 @@ ## Quick Install -* `pip install versioneer` to somewhere to your $PATH -* add a `[versioneer]` section to your setup.cfg (see below) -* run `versioneer install` in your source tree, commit the results +Versioneer provides two installation modes. The "classic" vendored mode installs +a copy of versioneer into your repository. The experimental build-time dependency mode +is intended to allow you to skip this step and simplify the process of upgrading. + +### Vendored mode + +* `pip install versioneer` to somewhere in your $PATH + * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is + available, so you can also use `conda install -c conda-forge versioneer` +* add a `[tool.versioneer]` section to your `pyproject.toml` or a + `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md)) + * Note that you will need to add `tomli; python_version < "3.11"` to your + build-time dependencies if you use `pyproject.toml` +* run `versioneer install --vendor` in your source tree, commit the results +* verify version information with `python setup.py version` + +### Build-time dependency mode + +* `pip install versioneer` to somewhere in your $PATH + * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is + available, so you can also use `conda install -c conda-forge versioneer` +* add a `[tool.versioneer]` section to your `pyproject.toml` or a + `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md)) +* add `versioneer` (with `[toml]` extra, if configuring in `pyproject.toml`) + to the `requires` key of the `build-system` table in `pyproject.toml`: + ```toml + [build-system] + requires = ["setuptools", "versioneer[toml]"] + build-backend = "setuptools.build_meta" + ``` +* run `versioneer install --no-vendor` in your source tree, commit the results +* verify version information with `python setup.py version` ## Version Identifiers @@ -61,7 +85,7 @@ for example `git describe --tags --dirty --always` reports things like "0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the 0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has -uncommitted changes. +uncommitted changes). The version identifier is used for multiple purposes: @@ -166,7 +190,7 @@ Some situations are known to cause problems for Versioneer. This details the most significant ones. More can be found on Github -[issues page](https://github.com/warner/python-versioneer/issues). +[issues page](https://github.com/python-versioneer/python-versioneer/issues). ### Subprojects @@ -180,7 +204,7 @@ `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI distributions (and upload multiple independently-installable tarballs). * Source trees whose main purpose is to contain a C library, but which also - provide bindings to Python (and perhaps other langauges) in subdirectories. + provide bindings to Python (and perhaps other languages) in subdirectories. Versioneer will look for `.git` in parent directories, and most operations should get the right version string. However `pip` and `setuptools` have bugs @@ -194,9 +218,9 @@ Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in some later version. -[Bug #38](https://github.com/warner/python-versioneer/issues/38) is tracking +[Bug #38](https://github.com/python-versioneer/python-versioneer/issues/38) is tracking this issue. The discussion in -[PR #61](https://github.com/warner/python-versioneer/pull/61) describes the +[PR #61](https://github.com/python-versioneer/python-versioneer/pull/61) describes the issue from the Versioneer side in more detail. [pip PR#3176](https://github.com/pypa/pip/pull/3176) and [pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve @@ -224,31 +248,20 @@ cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into a different virtualenv), so this can be surprising. -[Bug #83](https://github.com/warner/python-versioneer/issues/83) describes +[Bug #83](https://github.com/python-versioneer/python-versioneer/issues/83) describes this one, but upgrading to a newer version of setuptools should probably resolve it. -### Unicode version strings - -While Versioneer works (and is continually tested) with both Python 2 and -Python 3, it is not entirely consistent with bytes-vs-unicode distinctions. -Newer releases probably generate unicode version strings on py2. It's not -clear that this is wrong, but it may be surprising for applications when then -write these strings to a network connection or include them in bytes-oriented -APIs like cryptographic checksums. - -[Bug #71](https://github.com/warner/python-versioneer/issues/71) investigates -this question. - ## Updating Versioneer To upgrade your project to a new release of Versioneer, do the following: * install the new Versioneer (`pip install -U versioneer` or equivalent) -* edit `setup.cfg`, if necessary, to include any new configuration settings - indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details. -* re-run `versioneer install` in your source tree, to replace +* edit `setup.cfg` and `pyproject.toml`, if necessary, + to include any new configuration settings indicated by the release notes. + See [UPGRADING](./UPGRADING.md) for details. +* re-run `versioneer install --[no-]vendor` in your source tree, to replace `SRC/_version.py` * commit any changed files @@ -265,33 +278,56 @@ direction and include code from all supported VCS systems, reducing the number of intermediate scripts. +## Similar projects + +* [setuptools_scm](https://github.com/pypa/setuptools_scm/) - a non-vendored build-time + dependency +* [minver](https://github.com/jbweston/miniver) - a lightweight reimplementation of + versioneer +* [versioningit](https://github.com/jwodder/versioningit) - a PEP 518-based setuptools + plugin ## License To make Versioneer easier to embed, all its code is dedicated to the public domain. The `_version.py` that it creates is also in the public domain. -Specifically, both are released under the Creative Commons "Public Domain -Dedication" license (CC0-1.0), as described in -https://creativecommons.org/publicdomain/zero/1.0/ . +Specifically, both are released under the "Unlicense", as described in +https://unlicense.org/. -""" +[pypi-image]: https://img.shields.io/pypi/v/versioneer.svg +[pypi-url]: https://pypi.python.org/pypi/versioneer/ +[travis-image]: +https://img.shields.io/travis/com/python-versioneer/python-versioneer.svg +[travis-url]: https://travis-ci.com/github/python-versioneer/python-versioneer -from __future__ import print_function +""" +# pylint:disable=invalid-name,import-outside-toplevel,missing-function-docstring +# pylint:disable=missing-class-docstring,too-many-branches,too-many-statements +# pylint:disable=raise-missing-from,too-many-lines,too-many-locals,import-error +# pylint:disable=too-few-public-methods,redefined-outer-name,consider-using-with +# pylint:disable=attribute-defined-outside-init,too-many-arguments -try: - import configparser -except ImportError: - import ConfigParser as configparser +import configparser import errno import json import os import re import subprocess import sys +import functools from pkg_resources import parse_version +have_tomllib = True +if sys.version_info >= (3, 11): + import tomllib +else: + try: + import tomli as tomllib + except ImportError: + have_tomllib = False + class VersioneerConfig: """Container for Versioneer configuration parameters.""" @@ -325,12 +361,12 @@ def get_root(): # module-import table will cache the first one. So we can't use # os.path.dirname(__file__), as that will find whichever # versioneer.py was first imported, even in later projects. - me = os.path.realpath(os.path.abspath(__file__)) - me_dir = os.path.normcase(os.path.splitext(me)[0]) + my_path = os.path.realpath(os.path.abspath(__file__)) + me_dir = os.path.normcase(os.path.splitext(my_path)[0]) vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) - if me_dir != vsr_dir: + if me_dir != vsr_dir and "VERSIONEER_PEP518" not in globals(): print("Warning: build in %s is using versioneer.py from %s" - % (os.path.dirname(me), versioneer_py)) + % (os.path.dirname(my_path), versioneer_py)) except NameError: pass return root @@ -338,31 +374,38 @@ def get_root(): def get_config_from_root(root): """Read the project setup.cfg file to determine Versioneer config.""" - # This might raise EnvironmentError (if setup.cfg is missing), or + # This might raise OSError (if setup.cfg is missing), or # configparser.NoSectionError (if it lacks a [versioneer] section), or # configparser.NoOptionError (if it lacks "VCS="). See the docstring at # the top of versioneer.py for instructions on writing your setup.cfg . + pyproject_toml = os.path.join(root, "pyproject.toml") setup_cfg = os.path.join(root, "setup.cfg") - parser = configparser.SafeConfigParser() - with open(setup_cfg, "r") as f: - parser.readfp(f) - VCS = parser.get("versioneer", "VCS") # mandatory + section = None + if os.path.exists(pyproject_toml) and have_tomllib: + try: + with open(pyproject_toml, 'rb') as fobj: + pp = tomllib.load(fobj) + section = pp['tool']['versioneer'] + except (tomllib.TOMLDecodeError, KeyError): + pass + if not section: + parser = configparser.ConfigParser() + with open(setup_cfg) as cfg_file: + parser.read_file(cfg_file) + parser.get("versioneer", "VCS") # raise error if missing - def get(parser, name): - if parser.has_option("versioneer", name): - return parser.get("versioneer", name) - return None + section = parser["versioneer"] cfg = VersioneerConfig() - cfg.VCS = VCS - cfg.style = get(parser, "style") or "" - cfg.versionfile_source = get(parser, "versionfile_source") - cfg.versionfile_build = get(parser, "versionfile_build") - cfg.tag_prefix = get(parser, "tag_prefix") - if cfg.tag_prefix in ("''", '""'): + cfg.VCS = section['VCS'] + cfg.style = section.get("style", "") + cfg.versionfile_source = section.get("versionfile_source") + cfg.versionfile_build = section.get("versionfile_build") + cfg.tag_prefix = section.get("tag_prefix") + if cfg.tag_prefix in ("''", '""', None): cfg.tag_prefix = "" - cfg.parentdir_prefix = get(parser, "parentdir_prefix") - cfg.verbose = get(parser, "verbose") + cfg.parentdir_prefix = section.get("parentdir_prefix") + cfg.verbose = section.get("verbose") return cfg @@ -376,15 +419,11 @@ class NotThisMethod(Exception): def register_vcs_handler(vcs, method): # decorator - """Decorator to mark a method as the handler for a particular VCS.""" - + """Create decorator to mark a method as the handler of a VCS.""" def decorate(f): """Store f in HANDLERS[vcs][method].""" - if vcs not in HANDLERS: - HANDLERS[vcs] = {} - HANDLERS[vcs][method] = f + HANDLERS.setdefault(vcs, {})[method] = f return f - return decorate @@ -392,17 +431,25 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): """Call the given command(s).""" assert isinstance(commands, list) - p = None - for c in commands: + process = None + + popen_kwargs = {} + if sys.platform == "win32": + # This hides the console window if pythonw.exe is used + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + popen_kwargs["startupinfo"] = startupinfo + + for command in commands: try: - dispcmd = str([c] + args) + dispcmd = str([command] + args) # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) + process = subprocess.Popen([command] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None), **popen_kwargs) break - except EnvironmentError: + except OSError: e = sys.exc_info()[1] if e.errno == errno.ENOENT: continue @@ -414,26 +461,25 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, if verbose: print("unable to find command, tried %s" % (commands,)) return None, None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: + stdout = process.communicate()[0].strip().decode('utf-8') + if process.returncode != 0: if verbose: print("unable to run %s (error)" % dispcmd) print("stdout was %s" % stdout) - return None, p.returncode - return stdout, p.returncode + return None, process.returncode + return stdout, process.returncode -LONG_VERSION_PY['git'] = ''' +LONG_VERSION_PY['git'] = r''' # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build # directories (produced by setup.py build) will contain a much shorter file # that just contains the computed version number. -# This file is released into the public domain. Generated by -# versioneer-0.18 (https://github.com/warner/python-versioneer) +# This file is released into the public domain. +# Generated by versioneer-0.28 +# https://github.com/python-versioneer/python-versioneer """Git implementation of _version.py.""" @@ -442,6 +488,7 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, import re import subprocess import sys +import functools def get_keywords(): @@ -484,7 +531,7 @@ class NotThisMethod(Exception): def register_vcs_handler(vcs, method): # decorator - """Decorator to mark a method as the handler for a particular VCS.""" + """Create decorator to mark a method as the handler of a VCS.""" def decorate(f): """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: @@ -498,17 +545,25 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): """Call the given command(s).""" assert isinstance(commands, list) - p = None - for c in commands: + process = None + + popen_kwargs = {} + if sys.platform == "win32": + # This hides the console window if pythonw.exe is used + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + popen_kwargs["startupinfo"] = startupinfo + + for command in commands: try: - dispcmd = str([c] + args) + dispcmd = str([command] + args) # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) + process = subprocess.Popen([command] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None), **popen_kwargs) break - except EnvironmentError: + except OSError: e = sys.exc_info()[1] if e.errno == errno.ENOENT: continue @@ -520,15 +575,13 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, if verbose: print("unable to find command, tried %%s" %% (commands,)) return None, None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: + stdout = process.communicate()[0].strip().decode() + if process.returncode != 0: if verbose: print("unable to run %%s (error)" %% dispcmd) print("stdout was %%s" %% stdout) - return None, p.returncode - return stdout, p.returncode + return None, process.returncode + return stdout, process.returncode def versions_from_parentdir(parentdir_prefix, root, verbose): @@ -540,15 +593,14 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): """ rootdirs = [] - for i in range(3): + for _ in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): return {"version": dirname[len(parentdir_prefix):], "full-revisionid": None, "dirty": False, "error": None, "date": None} - else: - rootdirs.append(root) - root = os.path.dirname(root) # up a level + rootdirs.append(root) + root = os.path.dirname(root) # up a level if verbose: print("Tried directories %%s but none started with prefix %%s" %% @@ -565,22 +617,21 @@ def git_get_keywords(versionfile_abs): # _version.py. keywords = {} try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - f.close() - except EnvironmentError: + with open(versionfile_abs, "r") as fobj: + for line in fobj: + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + except OSError: pass return keywords @@ -588,10 +639,14 @@ def git_get_keywords(versionfile_abs): @register_vcs_handler("git", "keywords") def git_versions_from_keywords(keywords, tag_prefix, verbose): """Get version information from git keywords.""" - if not keywords: - raise NotThisMethod("no keywords at all, weird") + if "refnames" not in keywords: + raise NotThisMethod("Short version file found") date = keywords.get("date") if date is not None: + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] + # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 # -like" string, which we must then edit to make compliant), because @@ -604,11 +659,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) + refs = {r.strip() for r in refnames.strip("()").split(",")} # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %%d @@ -617,7 +672,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) + tags = {r for r in refs if re.search(r'\d', r)} if verbose: print("discarding '%%s', no digits" %% ",".join(refs - tags)) if verbose: @@ -626,6 +681,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): r = ref[len(tag_prefix):] + # Filter out refs that exactly match prefix or that don't start + # with a number once the prefix is stripped (mostly a concern + # when prefix is '') + if not re.match(r'\d', r): + continue if verbose: print("picking %%s" %% r) return {"version": r, @@ -641,7 +701,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): @register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): +def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* @@ -652,8 +712,15 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=True) + # GIT_DIR can interfere with correct operation of Versioneer. + # It may be intended to be passed to the Versioneer-versioned project, + # but that should not change where we get our version from. + env = os.environ.copy() + env.pop("GIT_DIR", None) + runner = functools.partial(runner, env=env) + + _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=not verbose) if rc != 0: if verbose: print("Directory %%s not under git control" %% root) @@ -661,15 +728,15 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%%s*" %% tag_prefix], - cwd=root) + describe_out, rc = runner(GITS, [ + "describe", "--tags", "--dirty", "--always", "--long", + "--match", "{}[[:digit:]]*".format(tag_prefix) + ], cwd=root) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() - full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() @@ -679,6 +746,39 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None + branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], + cwd=root) + # --abbrev-ref was added in git-1.6.3 + if rc != 0 or branch_name is None: + raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") + branch_name = branch_name.strip() + + if branch_name == "HEAD": + # If we aren't exactly on a branch, pick a branch which represents + # the current commit. If all else fails, we are on a branchless + # commit. + branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) + # --contains was added in git-1.5.4 + if rc != 0 or branches is None: + raise NotThisMethod("'git branch --contains' returned error") + branches = branches.split("\n") + + # Remove the first line if we're running detached + if "(" in branches[0]: + branches.pop(0) + + # Strip off the leading "* " from the list of branches. + branches = [branch[2:] for branch in branches] + if "master" in branches: + branch_name = "master" + elif not branches: + branch_name = None + else: + # Pick the first branch that is returned. Good or bad. + branch_name = branches[0] + + pieces["branch"] = branch_name + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out @@ -695,7 +795,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # TAG-NUM-gHEX mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) if not mo: - # unparseable. Maybe git-describe is misbehaving? + # unparsable. Maybe git-describe is misbehaving? pieces["error"] = ("unable to parse git-describe output: '%%s'" %% describe_out) return pieces @@ -720,13 +820,14 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): else: # HEX: no tags pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) - pieces["distance"] = int(count_out) # total number of commits + out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) + pieces["distance"] = len(out.split()) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%%ci", "HEAD"], - cwd=root)[0].strip() + date = runner(GITS, ["show", "-s", "--format=%%ci", "HEAD"], cwd=root)[0].strip() + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces @@ -764,19 +865,67 @@ def render_pep440(pieces): return rendered -def render_pep440_pre(pieces): - """TAG[.post.devDISTANCE] -- No -dirty. +def render_pep440_branch(pieces): + """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . + + The ".dev0" means not master branch. Note that .dev0 sorts backwards + (a feature branch will appear "older" than the master branch). Exceptions: - 1: no tags. 0.post.devDISTANCE + 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0" + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+untagged.%%d.g%%s" %% (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def pep440_split_post(ver): + """Split pep440 version string at the post-release segment. + + Returns the release segments before the post-release and the + post-release version number (or -1 if no post-release segment is present). + """ + vc = str.split(ver, ".post") + return vc[0], int(vc[1] or 0) if len(vc) == 2 else None + + +def render_pep440_pre(pieces): + """TAG[.postN.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post0.devDISTANCE + """ + if pieces["closest-tag"]: if pieces["distance"]: - rendered += ".post.dev%%d" %% pieces["distance"] + # update the post release segment + tag_version, post_version = pep440_split_post(pieces["closest-tag"]) + rendered = tag_version + if post_version is not None: + rendered += ".post%%d.dev%%d" %% (post_version + 1, pieces["distance"]) + else: + rendered += ".post0.dev%%d" %% (pieces["distance"]) + else: + # no commits, use the tag as the version + rendered = pieces["closest-tag"] else: # exception #1 - rendered = "0.post.dev%%d" %% pieces["distance"] + rendered = "0.post0.dev%%d" %% pieces["distance"] return rendered @@ -807,49 +956,30 @@ def render_pep440_post(pieces): return rendered -def render_pep440_ricequant(pieces): - tag = pieces["closest-tag"] - parsed_tag = parse_version(tag) if tag else None - - working = working_version() - parsed_working = parse_version(working) +def render_pep440_post_branch(pieces): + """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . - rendered = working + The ".dev0" means not master branch. - if tag: - if parsed_working < parsed_tag: - if parsed_working.base_version == parsed_tag.base_version: - if not parsed_tag.is_postrelease: - raise Exception("Only post release tag allowed, tag %s" % tag) - if parsed_tag._version.post[0] != "post": - raise Exception("Only post release tag allowed, tag %s" % tag) - post_num = parsed_tag._version.post[1] - rendered += ".post%d" % (post_num + 1) - if pieces["distance"] or pieces["dirty"]: - rendered += ".dev%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dirty" - else: - raise Exception(f"Developing version can not go back in time: {working_version()} < {tag}") - elif parsed_working == parsed_tag: - if pieces["distance"] or pieces["dirty"]: - rendered += ".post1.dev%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dirty" - else: - if pieces["distance"] or pieces["dirty"]: - rendered += ".dev%d" % pieces["distance"] - else: + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%%d" %% pieces["distance"] + if pieces["branch"] != "master": rendered += ".dev0" - + rendered += plus_or_dot(pieces) + rendered += "g%%s" %% pieces["short"] if pieces["dirty"]: rendered += ".dirty" else: - if pieces["distance"] or pieces["dirty"]: - rendered += ".dev%d" % pieces["distance"] - else: + # exception #1 + rendered = "0.post%%d" %% pieces["distance"] + if pieces["branch"] != "master": rendered += ".dev0" - + rendered += "+g%%s" %% pieces["short"] if pieces["dirty"]: rendered += ".dirty" return rendered @@ -860,7 +990,7 @@ def render_pep440_old(pieces): The ".dev0" means dirty. - Eexceptions: + Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: @@ -931,14 +1061,16 @@ def render(pieces, style): if style == "pep440": rendered = render_pep440(pieces) + elif style == "pep440-branch": + rendered = render_pep440_branch(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) + elif style == "pep440-post-branch": + rendered = render_pep440_post_branch(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) - elif style == 'pep440-ricequant': - rendered = render_pep440_ricequant(pieces) elif style == "git-describe": rendered = render_git_describe(pieces) elif style == "git-describe-long": @@ -972,7 +1104,7 @@ def get_versions(): # versionfile_source is the relative path from the top of the source # tree (where the .git directory might live) to this file. Invert # this to find the root from __file__. - for i in cfg.versionfile_source.split('/'): + for _ in cfg.versionfile_source.split('/'): root = os.path.dirname(root) except NameError: return {"version": "0+unknown", "full-revisionid": None, @@ -1007,22 +1139,21 @@ def git_get_keywords(versionfile_abs): # _version.py. keywords = {} try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - f.close() - except EnvironmentError: + with open(versionfile_abs, "r") as fobj: + for line in fobj: + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + except OSError: pass return keywords @@ -1030,10 +1161,14 @@ def git_get_keywords(versionfile_abs): @register_vcs_handler("git", "keywords") def git_versions_from_keywords(keywords, tag_prefix, verbose): """Get version information from git keywords.""" - if not keywords: - raise NotThisMethod("no keywords at all, weird") + if "refnames" not in keywords: + raise NotThisMethod("Short version file found") date = keywords.get("date") if date is not None: + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] + # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 # -like" string, which we must then edit to make compliant), because @@ -1046,11 +1181,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) + refs = {r.strip() for r in refnames.strip("()").split(",")} # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d @@ -1059,7 +1194,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) + tags = {r for r in refs if re.search(r'\d', r)} if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: @@ -1068,6 +1203,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): r = ref[len(tag_prefix):] + # Filter out refs that exactly match prefix or that don't start + # with a number once the prefix is stripped (mostly a concern + # when prefix is '') + if not re.match(r'\d', r): + continue if verbose: print("picking %s" % r) return {"version": r, @@ -1091,7 +1231,7 @@ def git_tracking_branch(): @register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): +def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* @@ -1102,8 +1242,15 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=True) + # GIT_DIR can interfere with correct operation of Versioneer. + # It may be intended to be passed to the Versioneer-versioned project, + # but that should not change where we get our version from. + env = os.environ.copy() + env.pop("GIT_DIR", None) + runner = functools.partial(runner, env=env) + + _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=not verbose) if rc != 0: if verbose: print("Directory %s not under git control" % root) @@ -1111,15 +1258,15 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%s*" % tag_prefix], - cwd=root) + describe_out, rc = runner(GITS, [ + "describe", "--tags", "--dirty", "--always", "--long", + "--match", "{}[[:digit:]]*".format(tag_prefix) + ], cwd=root) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() - full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() @@ -1129,6 +1276,39 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None + branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], + cwd=root) + # --abbrev-ref was added in git-1.6.3 + if rc != 0 or branch_name is None: + raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") + branch_name = branch_name.strip() + + if branch_name == "HEAD": + # If we aren't exactly on a branch, pick a branch which represents + # the current commit. If all else fails, we are on a branchless + # commit. + branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) + # --contains was added in git-1.5.4 + if rc != 0 or branches is None: + raise NotThisMethod("'git branch --contains' returned error") + branches = branches.split("\n") + + # Remove the first line if we're running detached + if "(" in branches[0]: + branches.pop(0) + + # Strip off the leading "* " from the list of branches. + branches = [branch[2:] for branch in branches] + if "master" in branches: + branch_name = "master" + elif not branches: + branch_name = None + else: + # Pick the first branch that is returned. Good or bad. + branch_name = branches[0] + + pieces["branch"] = branch_name + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out @@ -1145,7 +1325,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # TAG-NUM-gHEX mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) if not mo: - # unparseable. Maybe git-describe is misbehaving? + # unparsable. Maybe git-describe is misbehaving? pieces["error"] = ("unable to parse git-describe output: '%s'" % describe_out) return pieces @@ -1170,19 +1350,20 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): else: # HEX: no tags pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) - pieces["distance"] = int(count_out) # total number of commits + out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) + pieces["distance"] = len(out.split()) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], - cwd=root)[0].strip() + date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces -def do_vcs_install(manifest_in, versionfile_source, ipy): +def do_vcs_install(versionfile_source, ipy): """Git-specific installation logic for Versioneer. For Git, this means creating/changing .gitattributes to mark _version.py @@ -1191,31 +1372,31 @@ def do_vcs_install(manifest_in, versionfile_source, ipy): GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] - files = [manifest_in, versionfile_source] + files = [versionfile_source] if ipy: files.append(ipy) - try: - me = __file__ - if me.endswith(".pyc") or me.endswith(".pyo"): - me = os.path.splitext(me)[0] + ".py" - versioneer_file = os.path.relpath(me) - except NameError: - versioneer_file = "versioneer.py" - files.append(versioneer_file) + if "VERSIONEER_PEP518" not in globals(): + try: + my_path = __file__ + if my_path.endswith((".pyc", ".pyo")): + my_path = os.path.splitext(my_path)[0] + ".py" + versioneer_file = os.path.relpath(my_path) + except NameError: + versioneer_file = "versioneer.py" + files.append(versioneer_file) present = False try: - f = open(".gitattributes", "r") - for line in f.readlines(): - if line.strip().startswith(versionfile_source): - if "export-subst" in line.strip().split()[1:]: - present = True - f.close() - except EnvironmentError: + with open(".gitattributes", "r") as fobj: + for line in fobj: + if line.strip().startswith(versionfile_source): + if "export-subst" in line.strip().split()[1:]: + present = True + break + except OSError: pass if not present: - f = open(".gitattributes", "a+") - f.write("%s export-subst\n" % versionfile_source) - f.close() + with open(".gitattributes", "a+") as fobj: + fobj.write("{} export-subst\n".format(versionfile_source)) files.append(".gitattributes") run_command(GITS, ["add", "--"] + files) @@ -1229,15 +1410,14 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): """ rootdirs = [] - for i in range(3): + for _ in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): return {"version": dirname[len(parentdir_prefix):], "full-revisionid": None, "dirty": False, "error": None, "date": None} - else: - rootdirs.append(root) - root = os.path.dirname(root) # up a level + rootdirs.append(root) + root = os.path.dirname(root) # up a level if verbose: print("Tried directories %s but none started with prefix %s" % @@ -1246,7 +1426,7 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): SHORT_VERSION_PY = """ -# This file was generated by 'versioneer.py' (0.18) from +# This file was generated by 'versioneer.py' (0.28) from # revision-control system data, or from the parent directory name of an # unpacked source archive. Distribution tarballs contain a pre-generated copy # of this file. @@ -1268,7 +1448,7 @@ def versions_from_file(filename): try: with open(filename) as f: contents = f.read() - except EnvironmentError: + except OSError: raise NotThisMethod("unable to read _version.py") mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", contents, re.M | re.S) @@ -1323,19 +1503,67 @@ def render_pep440(pieces): return rendered -def render_pep440_pre(pieces): - """TAG[.post.devDISTANCE] -- No -dirty. +def render_pep440_branch(pieces): + """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . + + The ".dev0" means not master branch. Note that .dev0 sorts backwards + (a feature branch will appear "older" than the master branch). Exceptions: - 1: no tags. 0.post.devDISTANCE + 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0" + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def pep440_split_post(ver): + """Split pep440 version string at the post-release segment. + + Returns the release segments before the post-release and the + post-release version number (or -1 if no post-release segment is present). + """ + vc = str.split(ver, ".post") + return vc[0], int(vc[1] or 0) if len(vc) == 2 else None + + +def render_pep440_pre(pieces): + """TAG[.postN.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post0.devDISTANCE + """ + if pieces["closest-tag"]: if pieces["distance"]: - rendered += ".post.dev%d" % pieces["distance"] + # update the post release segment + tag_version, post_version = pep440_split_post(pieces["closest-tag"]) + rendered = tag_version + if post_version is not None: + rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) + else: + rendered += ".post0.dev%d" % (pieces["distance"]) + else: + # no commits, use the tag as the version + rendered = pieces["closest-tag"] else: # exception #1 - rendered = "0.post.dev%d" % pieces["distance"] + rendered = "0.post0.dev%d" % pieces["distance"] return rendered @@ -1366,12 +1594,33 @@ def render_pep440_post(pieces): return rendered -def debug_info(): - root = get_root() - cfg = get_config_from_root(root) - pieces = git_pieces_from_vcs(cfg.tag_prefix, root, cfg.verbose) - return "Developing: %s, CurrentTag: %s, Distance: %d, Dirty: %s" % ( - working_version(), pieces["closest-tag"], pieces["distance"], pieces["dirty"]) +def render_pep440_post_branch(pieces): + """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . + + The ".dev0" means not master branch. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + return rendered def render_pep440_ricequant(pieces): @@ -1397,39 +1646,33 @@ def render_pep440_ricequant(pieces): rendered += ".dev%d" % (pieces["distance"]) elif pieces["distance"] == 0: rendered += ".post%d" % parsed_tag._version.post[1] - if pieces["dirty"]: - rendered += ".dirty" else: raise Exception("Developing version can not go back in time: %s < %s" % (working, tag)) # 如果最近的tag是正式版tag,那么就是在开发该系列的.post1 elif parsed_working == parsed_tag: if pieces["distance"] > 0: rendered += ".post1.dev%d" % (pieces["distance"]) - if pieces["dirty"]: - rendered += ".dirty" # 如果正在开发到是一个新的系列,那么就从该系列的.dev0开始 else: if pieces["distance"] >= 0: rendered += ".dev%d" % (pieces["distance"]) - - if pieces["dirty"]: - rendered += ".dirty" # 没有最近的tag等价于正在开发到是一个新的系列,那么就从该系列的.dev0开始 else: if pieces["distance"] >= 0: rendered += ".dev%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dirty" - tracking_branch = git_tracking_branch() # # 如果是dev和master分支或者hotfix分支来的,或者是一个tag,那就用pep440的版本号,否则带上git commit id if tracking_branch in ["origin/develop", "origin/master"] or tracking_branch.startswith("origin/hotfix/") or pieces[ "distance"] == 0: + if pieces["dirty"]: + rendered += ".dirty" return rendered - else: - rendered += "+g{}".format(pieces["short"]) - return rendered + + rendered += "+%s" % format(pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered def render_pep440_old(pieces): @@ -1437,7 +1680,7 @@ def render_pep440_old(pieces): The ".dev0" means dirty. - Eexceptions: + Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: @@ -1508,14 +1751,18 @@ def render(pieces, style): if style == "pep440": rendered = render_pep440(pieces) + elif style == "pep440-ricequant": + rendered = render_pep440_ricequant(pieces) + elif style == "pep440-branch": + rendered = render_pep440_branch(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) + elif style == "pep440-post-branch": + rendered = render_pep440_post_branch(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) - elif style == 'pep440-ricequant': - rendered = render_pep440_ricequant(pieces) elif style == "git-describe": rendered = render_git_describe(pieces) elif style == "git-describe-long": @@ -1613,8 +1860,12 @@ def get_version(): return get_versions()["version"] -def get_cmdclass(): - """Get the custom setuptools/distutils subclasses used by Versioneer.""" +def get_cmdclass(cmdclass=None): + """Get the custom setuptools subclasses used by Versioneer. + + If the package uses a different cmdclass (e.g. one from numpy), it + should be provide as an argument. + """ if "versioneer" in sys.modules: del sys.modules["versioneer"] # this fixes the "python setup.py develop" case (also 'install' and @@ -1628,12 +1879,12 @@ def get_cmdclass(): # parent is protected against the child's "import versioneer". By # removing ourselves from sys.modules here, before the child build # happens, we protect the child from the parent's versioneer too. - # Also see https://github.com/warner/python-versioneer/issues/52 + # Also see https://github.com/python-versioneer/python-versioneer/issues/52 - cmds = {} + cmds = {} if cmdclass is None else cmdclass.copy() - # we add "version" to both distutils and setuptools - from distutils.core import Command + # we add "version" to setuptools + from setuptools import Command class cmd_version(Command): description = "report generated version string" @@ -1654,10 +1905,9 @@ def run(self): print(" date: %s" % vers.get("date")) if vers["error"]: print(" error: %s" % vers["error"]) - cmds["version"] = cmd_version - # we override "build_py" in both distutils and setuptools + # we override "build_py" in setuptools # # most invocation pathways end up running build_py: # distutils/build -> build_py @@ -1672,11 +1922,14 @@ def run(self): # then does setup.py bdist_wheel, or sometimes setup.py install # setup.py egg_info -> ? + # pip install -e . and setuptool/editable_wheel will invoke build_py + # but the build_py command is not expected to copy any files. + # we override different "build_py" commands for both environments - if "setuptools" in sys.modules: - from setuptools.command.build_py import build_py as _build_py + if 'build_py' in cmds: + _build_py = cmds['build_py'] else: - from distutils.command.build_py import build_py as _build_py + from setuptools.command.build_py import build_py as _build_py class cmd_build_py(_build_py): def run(self): @@ -1684,6 +1937,10 @@ def run(self): cfg = get_config_from_root(root) versions = get_versions() _build_py.run(self) + if getattr(self, "editable_mode", False): + # During editable installs `.py` and data files are + # not copied to build_lib + return # now locate _version.py in the new build/ directory and replace # it with an updated value if cfg.versionfile_build: @@ -1691,9 +1948,40 @@ def run(self): cfg.versionfile_build) print("UPDATING %s" % target_versionfile) write_to_version_file(target_versionfile, versions) - cmds["build_py"] = cmd_build_py + if 'build_ext' in cmds: + _build_ext = cmds['build_ext'] + else: + from setuptools.command.build_ext import build_ext as _build_ext + + class cmd_build_ext(_build_ext): + def run(self): + root = get_root() + cfg = get_config_from_root(root) + versions = get_versions() + _build_ext.run(self) + if self.inplace: + # build_ext --inplace will only build extensions in + # build/lib<..> dir with no _version.py to write to. + # As in place builds will already have a _version.py + # in the module dir, we do not need to write one. + return + # now locate _version.py in the new build/ directory and replace + # it with an updated value + if not cfg.versionfile_build: + return + target_versionfile = os.path.join(self.build_lib, + cfg.versionfile_build) + if not os.path.exists(target_versionfile): + print("Warning: {} does not exist, skipping " + "version update. This can happen if you are running build_ext " + "without first running build_py.".format(target_versionfile)) + return + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, versions) + cmds["build_ext"] = cmd_build_ext + if "cx_Freeze" in sys.modules: # cx_freeze enabled? from cx_Freeze.dist import build_exe as _build_exe # nczeczulin reports that py2exe won't like the pep440-style string @@ -1723,15 +2011,14 @@ def run(self): "PARENTDIR_PREFIX": cfg.parentdir_prefix, "VERSIONFILE_SOURCE": cfg.versionfile_source, }) - cmds["build_exe"] = cmd_build_exe del cmds["build_py"] if 'py2exe' in sys.modules: # py2exe enabled? try: - from py2exe.distutils_buildexe import py2exe as _py2exe # py3 + from py2exe.setuptools_buildexe import py2exe as _py2exe except ImportError: - from py2exe.build_exe import py2exe as _py2exe # py2 + from py2exe.distutils_buildexe import py2exe as _py2exe class cmd_py2exe(_py2exe): def run(self): @@ -1753,14 +2040,50 @@ def run(self): "PARENTDIR_PREFIX": cfg.parentdir_prefix, "VERSIONFILE_SOURCE": cfg.versionfile_source, }) - cmds["py2exe"] = cmd_py2exe + # sdist farms its file list building out to egg_info + if 'egg_info' in cmds: + _egg_info = cmds['egg_info'] + else: + from setuptools.command.egg_info import egg_info as _egg_info + + class cmd_egg_info(_egg_info, object): + def find_sources(self): + # egg_info.find_sources builds the manifest list and writes it + # in one shot + super(cmd_egg_info, self).find_sources() + + # Modify the filelist and normalize it + root = get_root() + cfg = get_config_from_root(root) + self.filelist.append('versioneer.py') + if cfg.versionfile_source: + # There are rare cases where versionfile_source might not be + # included by default, so we must be explicit + self.filelist.append(cfg.versionfile_source) + self.filelist.sort() + self.filelist.remove_duplicates() + + # The write method is hidden in the manifest_maker instance that + # generated the filelist and was thrown away + # We will instead replicate their final normalization (to unicode, + # and POSIX-style paths) + from setuptools import unicode_utils + normalized = [unicode_utils.filesys_decode(f).replace(os.sep, '/') + for f in self.filelist.files] + + manifest_filename = os.path.join(self.egg_info, 'SOURCES.txt') + with open(manifest_filename, 'w') as fobj: + fobj.write('\n'.join(normalized)) + + cmds['egg_info'] = cmd_egg_info + # we override different "sdist" commands for both environments - if "setuptools" in sys.modules: - from setuptools.command.sdist import sdist as _sdist + if 'sdist' in cmds: + _sdist = cmds['sdist'] else: - from distutils.command.sdist import sdist as _sdist + from setuptools.command.sdist import sdist as _sdist class cmd_sdist(_sdist): def run(self): @@ -1782,7 +2105,6 @@ def make_release_tree(self, base_dir, files): print("UPDATING %s" % target_versionfile) write_to_version_file(target_versionfile, self._versioneer_generated_versions) - cmds["sdist"] = cmd_sdist return cmds @@ -1825,26 +2147,30 @@ def make_release_tree(self, base_dir, files): """ -INIT_PY_SNIPPET = """ +OLD_SNIPPET = """ from ._version import get_versions __version__ = get_versions()['version'] del get_versions """ +INIT_PY_SNIPPET = """ +from . import {0} +__version__ = {0}.get_versions()['version'] +""" + def do_setup(): - """Main VCS-independent setup function for installing Versioneer.""" + """Do main VCS-independent setup function for installing Versioneer.""" root = get_root() try: cfg = get_config_from_root(root) - except (EnvironmentError, configparser.NoSectionError, + except (OSError, configparser.NoSectionError, configparser.NoOptionError) as e: - if isinstance(e, (EnvironmentError, configparser.NoSectionError)): - print("Adding sample versioneer config to setup.cfg", - file=sys.stderr) + if isinstance(e, (OSError, configparser.NoSectionError)): + print("Adding sample versioneer config to setup.cfg") with open(os.path.join(root, "setup.cfg"), "a") as f: f.write(SAMPLE_CONFIG) - print(CONFIG_ERROR, file=sys.stderr) + print(CONFIG_ERROR) return 1 print(" creating %s" % cfg.versionfile_source) @@ -1863,54 +2189,28 @@ def do_setup(): try: with open(ipy, "r") as f: old = f.read() - except EnvironmentError: + except OSError: old = "" - if INIT_PY_SNIPPET not in old: + module = os.path.splitext(os.path.basename(cfg.versionfile_source))[0] + snippet = INIT_PY_SNIPPET.format(module) + if OLD_SNIPPET in old: + print(" replacing boilerplate in %s" % ipy) + with open(ipy, "w") as f: + f.write(old.replace(OLD_SNIPPET, snippet)) + elif snippet not in old: print(" appending to %s" % ipy) with open(ipy, "a") as f: - f.write(INIT_PY_SNIPPET) + f.write(snippet) else: print(" %s unmodified" % ipy) else: print(" %s doesn't exist, ok" % ipy) ipy = None - # Make sure both the top-level "versioneer.py" and versionfile_source - # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so - # they'll be copied into source distributions. Pip won't be able to - # install the package without this. - manifest_in = os.path.join(root, "MANIFEST.in") - simple_includes = set() - try: - with open(manifest_in, "r") as f: - for line in f: - if line.startswith("include "): - for include in line.split()[1:]: - simple_includes.add(include) - except EnvironmentError: - pass - # That doesn't cover everything MANIFEST.in can do - # (http://docs.python.org/2/distutils/sourcedist.html#commands), so - # it might give some false negatives. Appending redundant 'include' - # lines is safe, though. - if "versioneer.py" not in simple_includes: - print(" appending 'versioneer.py' to MANIFEST.in") - with open(manifest_in, "a") as f: - f.write("include versioneer.py\n") - else: - print(" 'versioneer.py' already in MANIFEST.in") - if cfg.versionfile_source not in simple_includes: - print(" appending versionfile_source ('%s') to MANIFEST.in" % - cfg.versionfile_source) - with open(manifest_in, "a") as f: - f.write("include %s\n" % cfg.versionfile_source) - else: - print(" versionfile_source already in MANIFEST.in") - # Make VCS-specific changes. For git, this means creating/changing # .gitattributes to mark _version.py for export-subst keyword # substitution. - do_vcs_install(manifest_in, cfg.versionfile_source, ipy) + do_vcs_install(cfg.versionfile_source, ipy) return 0 @@ -1951,11 +2251,18 @@ def scan_setup_py(): return errors +def setup_command(): + """Set up Versioneer and exit with appropriate error code.""" + errors = do_setup() + errors += scan_setup_py() + sys.exit(1 if errors else 0) + + def get_metadata(): setup_cfg = os.path.join(get_root(), "setup.cfg") - parser = configparser.SafeConfigParser() + parser = configparser.RawConfigParser() with open(setup_cfg, "r") as f: - parser.readfp(f) + parser.read_file(f) def get(section, parser, name): if parser.has_option(section, name): @@ -1975,150 +2282,7 @@ def package_name(): def working_version(): return get_metadata()["version"] - -def next_tag_name(): - root = get_root() - cfg = get_config_from_root(root) - pieces = git_pieces_from_vcs(cfg.tag_prefix, root, cfg.verbose) - - tag = pieces["closest-tag"] - parsed_tag = parse_version(tag) if tag else None - - working = working_version() - parsed_working = parse_version(working) - - rendered = working - - curr_version = get_version() - parsed_curr_version = parse_version(curr_version) - - if tag: - # 如果当前开发系列正式版小于最近的tag,那么当前开发系列和该tag必须属于同一个开发版本系列 - # 当前开发都没有做完,大于当前开发系列的tag不可能存 - # 因为大于当前开发系列的正式版, 该tag必然是post release - # 如果发现当前commit和最近的tag的commit相同,规定next tag不变. 这也限制只有最近tag后的第一个commit存在, - # 才会获取到next tag name - if parsed_working < parsed_tag: - if parsed_working.base_version == parsed_tag.base_version: - if not parsed_tag.is_postrelease: - raise Exception("Only post release tag allowed, tag %s" % tag) - if parsed_tag._version.post[0] != "post": - raise Exception("Only post release tag allowed, tag %s" % tag) - # 正在开发的版本不可能小于等于最近的tag - if parsed_curr_version <= parsed_tag: - raise Exception("Current version %s should > latest tag %s" % (curr_version, tag)) - if pieces["distance"] == 0: - return tag - - rendered += ".post%d" % parsed_curr_version._version.post[1] - else: - raise Exception( - "Developing version can not go back in time: %s(working version series) < %s(latest tag)" % working, - tag) - # 如果当前开发系列的正式版等于最近的tag,那么下一个tag必定是.post1 - elif parsed_working == parsed_tag: - if parsed_curr_version._version.post[0] != "post": - raise Exception("Current version should be a post version %s" % curr_version) - rendered += ".post%d" % parsed_curr_version._version.post[1] - - # 如果当前开发版本系列正式版大于最近的tag,那么下一个的tag将是当前开发系列的正式版tag - return rendered - - -def is_same_package(package_file): - curr_version = get_versions() - v = _get_version_py_in_tar(package_file) - if not v: - v = _get_version_py_in_zip(package_file) - if not v: - raise Exception("Can not get _version.py from %s" % package_file) - - from types import ModuleType - m = ModuleType("get_versions") - exec(v, m.__dict__) - version_info = m.get_versions() - return curr_version["full-revisionid"] == version_info["full-revisionid"] - - -def _get_package_dir(): - package = None - for d in os.listdir(): - if os.path.isdir(d): - if "__init__.py" in os.listdir(d): - package = d - return package - - -def _get_version_py_in_zip(package_file): - import zipfile - package_dir = _get_package_dir() - - if zipfile.is_zipfile(package_file): - f = zipfile.ZipFile(package_file) - il = f.infolist() - if len(il) == 0: - raise Exception("No member found in %s" % package_file) - for i in il: - if i.filename == os.path.join(package_dir, "_version.py"): - return f.open(i).read() - return None - - -def _get_version_py_in_tar(package_file): - import tarfile - package_dir = _get_package_dir() - if tarfile.is_tarfile(package_file): - f = tarfile.open(package_file) - ms = f.getmembers() - if len(ms) == 0: - raise Exception("No members found in %s" % package_file) - for m in ms: - if os.path.join(ms[0].name, package_dir, "_version.py") == m.path: - version_file = f.extractfile(m) - return version_file.read() - return None - - -def get_package_name_in_pypi(version=None): - from pip._internal.commands.install import InstallCommand - from pip._internal.cli.cmdoptions import make_target_python - - class ListVersionCommand(InstallCommand): - def __init__(self, name, summary): - super().__init__(name, summary) - - def get_version_list(self): - with self.main_context(): - options, args = self.parse_args([package_name()]) - session = self.get_default_session(options=options) - target_python = make_target_python(options) - finder = self._build_package_finder( - options=options, - session=session, - target_python=target_python, - ignore_requires_python=options.ignore_requires_python, - ) - versions = {} - for c in finder.find_all_candidates(package_name()): - v = str(c.version) - f = c.link.filename - if (v in versions and f.endswith(".whl")) or v not in versions: - versions[v] = f - return versions - - if not version: - version = get_version() - c = ListVersionCommand("listversion", summary="list versions") - versions = c.get_version_list() - return versions.get(version, "") - - if __name__ == "__main__": cmd = sys.argv[1] if cmd == "setup": - errors = do_setup() - errors += scan_setup_py() - if errors: - sys.exit(1) - elif cmd == "is_pypi_has": - get_package_name_in_pypi(sys.argv[2]) \ No newline at end of file + setup_command()