mirror of
https://github.com/microsoft/qlib.git
synced 2026-07-03 02:50:58 +08:00
Merge pull request #1576 from microsoft/xuyang1/add_config_and_code_dump_task
refine workflow and prompts
This commit is contained in:
@@ -29,3 +29,4 @@ class Config(Singleton):
|
||||
)
|
||||
self.debug_mode = os.getenv("DEBUG_MODE") == "True" if os.getenv("DEBUG_MODE") is not None else False
|
||||
self.workspace = os.getenv("WORKSPACE") if os.getenv("WORKSPACE") is not None else "./finco_workspace"
|
||||
self.max_past_message_include = int(os.getenv("MAX_PAST_MESSAGE_INCLUDE") or 6) // 2 * 2
|
||||
|
||||
@@ -27,7 +27,7 @@ class APIBackend(Singleton):
|
||||
json.load(open(self.cache_file_location, "r")) if os.path.exists(self.cache_file_location) else {}
|
||||
)
|
||||
|
||||
def build_messages_and_create_chat_completion(self, user_prompt, system_prompt=None):
|
||||
def build_messages_and_create_chat_completion(self, user_prompt, system_prompt=None, former_messages=[], **kwargs):
|
||||
"""build the messages to avoid implementing several redundant lines of code"""
|
||||
cfg = Config()
|
||||
# TODO: system prompt should always be provided. In development stage we can use default value
|
||||
@@ -35,20 +35,23 @@ class APIBackend(Singleton):
|
||||
try:
|
||||
system_prompt = cfg.system_prompt
|
||||
except AttributeError:
|
||||
get_module_logger("finco").warning("system_prompt is not set, using default value.")
|
||||
FinCoLog().warning("system_prompt is not set, using default value.")
|
||||
system_prompt = "You are an AI assistant who helps to answer user's questions about finance."
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": system_prompt,
|
||||
},
|
||||
}
|
||||
]
|
||||
messages.extend(former_messages[-1*cfg.max_past_message_include:])
|
||||
messages.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": user_prompt,
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
fcl = FinCoLog()
|
||||
response = self.try_create_chat_completion(messages=messages)
|
||||
response = self.try_create_chat_completion(messages=messages, **kwargs)
|
||||
fcl.log_message(messages)
|
||||
fcl.log_response(response)
|
||||
return response
|
||||
@@ -59,7 +62,7 @@ class APIBackend(Singleton):
|
||||
try:
|
||||
response = self.create_chat_completion(**kwargs)
|
||||
return response
|
||||
except openai.error.RateLimitError as e:
|
||||
except (openai.error.RateLimitError, openai.error.Timeout) as e:
|
||||
print(e)
|
||||
print(f"Retrying {i+1}th time...")
|
||||
time.sleep(1)
|
||||
@@ -75,8 +78,9 @@ class APIBackend(Singleton):
|
||||
) -> str:
|
||||
|
||||
if self.debug_mode:
|
||||
if messages[1]["content"] in self.cache:
|
||||
return self.cache[messages[1]["content"]]
|
||||
key = json.dumps(messages)
|
||||
if key in self.cache:
|
||||
return self.cache[key]
|
||||
|
||||
if temperature is None:
|
||||
temperature = self.cfg.temperature
|
||||
@@ -96,6 +100,6 @@ class APIBackend(Singleton):
|
||||
)
|
||||
resp = response.choices[0].message["content"]
|
||||
if self.debug_mode:
|
||||
self.cache[messages[1]["content"]] = resp
|
||||
self.cache[key] = resp
|
||||
json.dump(self.cache, open(self.cache_file_location, "w"))
|
||||
return resp
|
||||
|
||||
@@ -17,11 +17,6 @@ class PromptTemplate(Singleton):
|
||||
continue
|
||||
self.__setattr__(k, Template(v))
|
||||
|
||||
for target_name, module_to_render_params in _template["mods"].items():
|
||||
for module_name, params in module_to_render_params.items():
|
||||
self.__setattr__(f"{target_name}_{module_name}",
|
||||
Template(self.__getattribute__(target_name).render(**params)))
|
||||
|
||||
def get(self, key: str):
|
||||
return self.__dict__.get(key, Template(""))
|
||||
|
||||
|
||||
@@ -25,28 +25,40 @@ WorkflowTask_user : |-
|
||||
SLPlanTask_system : |-
|
||||
Your task is to design the 6 crucial components in Qlib (Dataset, DataHandler, Model, Record, Strategy, Backtest) ensuring the workflow can meet the user's requirements.
|
||||
|
||||
For each component, you first point out whether to use default module in Qlib or implement the new module (Default or Personized). Default module means the class has already be implemented by Qlib which can be found in document and source code. Default class can be directed called from config file without additional implementation. Personized module means new python class is implemented and called from config file. You should always provide the reason of your choice.
|
||||
The predefined class in Qlib modules can be listed in format of {module_path}-{class name}:
|
||||
Dataset: {qlib.data.dataset}-{DatasetH}, {qlib.contrib.data.dataset}-{MTSDatasetH}
|
||||
DataHandler: {qlib.contrib.data.handler}-{Alpha158}, {qlib.contrib.data.handler}-{Alpha158vwap}, {qlib.contrib.data.handler}-{Alpha360}, {qlib.contrib.data.handler}-{Alpha360vwap}, {qlib.data.dataset.loader}-{QlibDataLoader}
|
||||
Model: {qlib.contrib.model.catboost_model}-{CatBoostModel}, {qlib.contrib.model.double_ensemble}-{DoubleEnsembleModel}, {qlib.contrib.model.gbdt}-{LGBModel}, {qlib.contrib.model.highfreq_gdbt_model}-{HFLGBModel}, {qlib.contrib.model.linear}-{LinearModel}, {qlib.contrib.model.pytorch_adarnn}-{AdaRNNModel}, {qlib.contrib.model.pytorch_add}-{ADD}, {qlib.contrib.model.pytorch_alstm_ts}-{ALSTM}, {qlib.contrib.model.pytorch_alstm}-{ALSTM}, {qlib.contrib.model.pytorch_gats}-{GATs}, {qlib.contrib.model.pytorch_gats_ts}-{GATs}, {qlib.contrib.model.pytorch_gru}-{GRU}, {qlib.contrib.model.pytorch_gru_ts}-{GRU}, {qlib.contrib.model.pytorch_hist}-{HIST}, {qlib.contrib.model.pytorch_igmtf}-{IGMTF}, {qlib.contrib.model.pytorch_localformer}-{LocalformerModel}, {qlib.contrib.model.pytorch_localformer_ts}-{LocalformerModel}, {qlib.contrib.model.pytorch_lstm}-{LSTM}, {qlib.contrib.model.pytorch_lstm_ts}-{LSTM}, {qlib.contrib.model.pytorch_nn}-{DNNModelPytorch}, {qlib.contrib.model.pytorch_sfm}-{SFM}, {qlib.contrib.model.pytorch_tabnet}-{TabnetModel}, {qlib.contrib.model.pytorch_tcn_ts}-{TCN}, {qlib.contrib.model.pytorch_tcn}-{TCN}, {qlib.contrib.model.pytorch_tcts.}-{TCTS}, {qlib.contrib.model.pytorch_tra}-{TRA}, {qlib.contrib.model.pytorch_transformer}-{TransformerModel}, {qlib.contrib.model.pytorch_transformer_ts}-{TransformerModel}, {qlib.contrib.model.xgboost}-{XGBModel}
|
||||
Record: {qlib.workflow.record_temp}-{SignalRecord}, {qlib.workflow.record_temp}-{SigAnaRecord},
|
||||
Strategy: {qlib.contrib.strategy}-{TopkDropoutStrategy}, {qlib.contrib.strategy}-{WeightStrategyBase}, {qlib.contrib.strategy}-{EnhancedIndexingStrategy}, {qlib.contrib.strategy}-{TWAPStrategy}, {qlib.contrib.strategy}-{SBBStrategyBase}, {qlib.contrib.strategy}-{SBBStrategyEMA}, {qlib.contrib.strategy}-{SoftTopkStrategy}
|
||||
The list will be called as "predefined classes" in the following prompts.
|
||||
|
||||
For each component, you first point out whether to use default module in Qlib or implement the new module (Default or Personized). Default module means picking one of the predefined classes to meet the user's requirement. Personized module means new python class implemented and called from config file. The new class should always inherit from one of the class in the predefined classes.
|
||||
|
||||
If choose Default, provide the predefined class after the choice, otherwise, provide the predefined class your code plans to inherit from. the format of predefined class should follow the previous format. Backtest module has no predefined class so you don't need to provide.
|
||||
|
||||
If the user's requirement can be met with Default module, always use default module to avoid code error!!!
|
||||
|
||||
Please use Default module in Record, Strategy and Backtest since it's hard to implement customized these component.
|
||||
|
||||
The user will provide the requirements, you will provide only the output the choice in exact format specified below with no explanation or conversation. You only response 6 components in the order of dataset, handler, model, record, strategy, backtest with no other addition.
|
||||
|
||||
Example input:
|
||||
Help me build a low turnover quant investment strategy that focus more on long turn return in China a stock market. I have some data in csv format and I want to merge them with the data in Qlib.
|
||||
Help me build a low turnover quant investment strategy that focus more on long turn return in China a stock market. I want to use a big LSTM model and add several MLP layer before the head.
|
||||
|
||||
Example output:
|
||||
components:
|
||||
- Dataset: (Personized) I will implement a CustomDataset inherited from DatasetH in qlib.data.dataset and merge the user-provided CSV data with the existing Qlib data. Because this will allow the user to leverage both their custom data and Qlib's data for the China A stock market.
|
||||
- Dataset: (Default) {qlib.data.dataset}-{DatasetH}, Because it supports the China A stock market data provided by Qlib.
|
||||
|
||||
- DataHandler: (Personized) I will implement a CustomDataHandler inherited from Alpha360 in qlib.contrib.data.handler to handle the merged dataset from the custom dataset. Because it is necessary to handle the combined data from Qlib's Alpha360 and the user's CSV file.
|
||||
- DataHandler: (Default) {qlib.contrib.data.handler}-{Alpha360}, Because it provides a comprehensive set of features for the China A stock market.
|
||||
|
||||
- Model: (Default) I will use LGBModel in qlib.contrib.model.gbdt for the low turnover strategy. Because it is a popular and efficient model for quant investment strategies and can capture long-term patterns in the data.
|
||||
- Model: (Personized) {qlib.contrib.model.pytorch_lstm}-{LSTM}, Because the user wants to use a big LSTM model with additional MLP layers before the head.
|
||||
|
||||
- Record: (Default) I will use SignalRecord in qlib.workflow.record_temp and SigAnaRecord in qlib.workflow.record_temp to save all the signals and the analysis results. Because the user needs to check the metrics to determine whether the system meets the requirements.
|
||||
- Record: (Default) {qlib.workflow.record_temp}-{SignalRecord}{qlib.workflow.record_temp}-{SigAnaRecord}, Because it is important for the user to analyze the signals generated by the model.
|
||||
|
||||
- Strategy: (Default) I will use TopkDropoutStrategy in qlib.contrib.strategy. Because it is a more robust strategy which saves turnover fee and focuses on long-term return.
|
||||
- Strategy: (Default) {qlib.contrib.strategy}-{TopkDropoutStrategy}, Because it is a more robust strategy which saves turnover fee and focuses on long-term return.
|
||||
|
||||
- Backtest: (Default) I will use the default backtest module in Qlib. Because it can tell the user a more real performance result of the model we build.
|
||||
- Backtest: (Default) Because it can tell the user a more real performance result of the model we build.
|
||||
|
||||
SLPlanTask_user : |-
|
||||
User input: '{{user_prompt}}'
|
||||
@@ -82,33 +94,432 @@ CMDTask_user : |-
|
||||
- User OS: "{{user_os}}"
|
||||
Example output:
|
||||
|
||||
ConfigActionTask_system : |-
|
||||
Your task is to write the config of the target component in Qlib(Dataset, DataHandler, Model, Record, Strategy, Backtest).
|
||||
HyperparameterActionTask_system : |-
|
||||
Your task is to determine the hyperparameters to initialize the target class of the target component in Qlib(Dataset, DataHandler, Model, Record, Strategy, Backtest). User will provide possible hyperparameter names, you can choose to use default value(Please provice exactly "Default" in response) or set personized value to better meet user's requirement.
|
||||
|
||||
Config means the yaml file in Qlib. You can find the default config in qlib/contrib/config_template. You can also find the config in Qlib document. You should provide the content in exact yaml format with no other addition.
|
||||
The predefined class in the target Qlib module can be listed in format of {module_path}-{class name}:
|
||||
{% if target_module == "Dataset" %}
|
||||
Dataset: {qlib.data.dataset}-{DatasetH}, {qlib.contrib.data.dataset}-{MTSDatasetH}
|
||||
{% elif target_module == "DataHandler" %}
|
||||
DataHandler: {qlib.contrib.data.handler}-{Alpha158}, {qlib.contrib.data.handler}-{Alpha158vwap}, {qlib.contrib.data.handler}-{Alpha360}, {qlib.contrib.data.handler}-{Alpha360vwap}, {qlib.data.dataset.loader}-{QlibDataLoader}
|
||||
{% elif target_module == "Model" %}
|
||||
Model: {qlib.contrib.model.catboost_model}-{CatBoostModel}, {qlib.contrib.model.double_ensemble}-{DoubleEnsembleModel}, {qlib.contrib.model.gbdt}-{LGBModel}, {qlib.contrib.model.highfreq_gdbt_model}-{HFLGBModel}, {qlib.contrib.model.linear}-{LinearModel}, {qlib.contrib.model.pytorch_adarnn}-{AdaRNNModel}, {qlib.contrib.model.pytorch_add}-{ADD}, {qlib.contrib.model.pytorch_alstm_ts}-{ALSTM}, {qlib.contrib.model.pytorch_alstm}-{ALSTM}, {qlib.contrib.model.pytorch_gats}-{GATs}, {qlib.contrib.model.pytorch_gats_ts}-{GATs}, {qlib.contrib.model.pytorch_gru}-{GRU}, {qlib.contrib.model.pytorch_gru_ts}-{GRU}, {qlib.contrib.model.pytorch_hist}-{HIST}, {qlib.contrib.model.pytorch_igmtf}-{IGMTF}, {qlib.contrib.model.pytorch_localformer}-{LocalformerModel}, {qlib.contrib.model.pytorch_localformer_ts}-{LocalformerModel}, {qlib.contrib.model.pytorch_lstm}-{LSTM}, {qlib.contrib.model.pytorch_lstm_ts}-{LSTM}, {qlib.contrib.model.pytorch_nn}-{DNNModelPytorch}, {qlib.contrib.model.pytorch_sfm}-{SFM}, {qlib.contrib.model.pytorch_tabnet}-{TabnetModel}, {qlib.contrib.model.pytorch_tcn_ts}-{TCN}, {qlib.contrib.model.pytorch_tcn}-{TCN}, {qlib.contrib.model.pytorch_tcts.}-{TCTS}, {qlib.contrib.model.pytorch_tra}-{TRA}, {qlib.contrib.model.pytorch_transformer}-{TransformerModel}, {qlib.contrib.model.pytorch_transformer_ts}-{TransformerModel}, {qlib.contrib.model.xgboost}-{XGBModel}
|
||||
{% elif target_module == "Record" %}
|
||||
Record: {qlib.workflow.record_temp}-{SignalRecord}, {qlib.workflow.record_temp}-{SigAnaRecord},
|
||||
{% elif target_module == "Strategy" %}
|
||||
Strategy: {qlib.contrib.strategy}-{TopkDropoutStrategy}, {qlib.contrib.strategy}-{WeightStrategyBase}, {qlib.contrib.strategy}-{EnhancedIndexingStrategy}, {qlib.contrib.strategy}-{TWAPStrategy}, {qlib.contrib.strategy}-{SBBStrategyBase}, {qlib.contrib.strategy}-{SBBStrategyEMA}, {qlib.contrib.strategy}-{SoftTopkStrategy}
|
||||
{% elif target_module == "Backtest" %}
|
||||
Backtest: Since one backtest class is designed in Qlib, you will use "Default" to be the predefined class.
|
||||
{% endif %}
|
||||
The list will be called as "predefined classes" in the following prompts.
|
||||
{% if target_module != "Backtest" %}
|
||||
You have chosen Default backtest module.
|
||||
{% elif choice == "Default" %}
|
||||
You have chosen several predefined classes from Qlib(more than one classes might be picked, you should provide the each class's hyperparameters):
|
||||
{%for module_path, class_name in classes%}{% raw %}{{% endraw %}{{module_path}}{% raw %}}{% endraw %}-{% raw %}{{% endraw %}{{class_name}}{% raw %}}{% endraw %}.{% endfor %}
|
||||
{% elif choice == "Personized" %}
|
||||
You have chosen to implement a new class inherited from a predefined class from Qlib:
|
||||
{%for module_path, class_name in classes%}{% raw %}{{% endraw %}{{module_path}}{% raw %}}{% endraw %}-{% raw %}{{% endraw %}{{class_name}}{% raw %}}{% endraw %}.{% endfor %}, you should also provide the each class's hyperparameters to initialize the class.
|
||||
{% endif %}
|
||||
Caution: User's hyperparameter list is to hint, you can add more hyperparameters if necessary. Especially, some hyperparameters are set through kwargs, so always consider these hyperparameters and add them if necessary!!!
|
||||
|
||||
The user has provided the requirements and made plan and reason to each component. You should strictly follow user's plan and you should provide the reason of your hyperparameter choices if exist and some suggestion if user wants to finetune the hyperparameters after the config. Default means you should only use classes in Qlib without any other new code while Personized has no such restriction. class in Qlib means Qlib has implemented the class and you can find it in Qlib document or source code.
|
||||
The user has provided the requirements, chose the predefined classes and made plan and reason to each component. You should strictly follow user's choice and you should provide the reason of your hyperparameter choices if exist and some suggestion if the user wants to finetune the hyperparameters after the hyperparameter.
|
||||
|
||||
"Config", "Reason" and "Improve suggestion" should always be provided with exactly the same letters.
|
||||
|
||||
{{target_component_desc}}
|
||||
|
||||
You only need to write the config of the target component in the exact format specified below with no explanation or conversation.
|
||||
You only need to response the hyperparameters in the exact format in exsample below with no explanation or conversation. "Hyperparameters:", "Reason:", "Improve suggestion:" are key tags so always include them in response.
|
||||
{% if target_module == "DataHandler" %}
|
||||
Qlib has these processors {processor_name}-{hyperparameter kwargs}:
|
||||
{DropnaProcessor}-{['fields_group']},{DropnaLabel}-{['fields_group']},{CSRankNorm}-{['fields_group']},{ProcessInf}-{[]},{Processor}-{[]},{MinMaxNorm}-{['fit_start_time', 'fit_end_time', 'fields_group']},{CSZFillna}-{['fields_group']},{TanhProcess}-{[]},{CSZScoreNorm}-{['fields_group', 'method']},{RobustZScoreNorm}-{['fit_start_time', 'fit_end_time', 'fields_group', 'clip_outlier']},{FilterCol}-{['fields_group', 'col_list']},{HashStockFormat}-{[]},{ZScoreNorm}-{['fit_start_time', 'fit_end_time', 'fields_group']},{DropCol}-{['col_list']},{Fillna}-{['fields_group', 'fill_value']}.
|
||||
You can choose some of them to use in {infer_processors} or {learn_processors} if necessary and pick the kwargs of them.
|
||||
freq should pick one from {year}/{quarter}/{month}/{week}/{day}.
|
||||
{% endif %}
|
||||
|
||||
Example input:
|
||||
user requirement: Help me build a low turnover quant investment strategy that focus more on long turn return in China a stock market. I have some data in csv format and I want to merge them with the data in Qlib.
|
||||
user plan:
|
||||
{{target_component_example_input}}
|
||||
target component: {{target_component}}
|
||||
user requirement: Help me build a low turnover quant investment strategy that focus more on long turn return in China a stock market. I want to use a big LSTM model and add several MLP layer before the head.
|
||||
user plan:{% if target_module == "Dataset" %}
|
||||
Dataset: (Default) {qlib.data.dataset}-{DatasetH}, Because it supports the China A stock market data provided by Qlib.
|
||||
{% elif target_module == "DataHandler" %}
|
||||
DataHandler: (Default) {qlib.contrib.data.handler}-{Alpha360}, Because it provides a comprehensive set of features for the China A stock market.
|
||||
{% elif target_module == "Model" %}
|
||||
Model: (Default) {qlib.contrib.model.gbdt}-{LGBModel}, Because it uses the LightGBM model requested by the user.
|
||||
{% elif target_module == "Record" %}
|
||||
Record: (Default) {qlib.workflow.record_temp}-{SignalRecord}{qlib.workflow.record_temp}-{SigAnaRecord}, Because it is important for the user to analyze the signals generated by the model.
|
||||
{% elif target_module == "Strategy" %}
|
||||
Strategy: (Default) {qlib.contrib.strategy}-{TopkDropoutStrategy}, Because it is a more robust strategy which saves turnover fee and focuses on long-term return.
|
||||
{% elif target_module == "Backtest" %}
|
||||
Backtest: (Default) Because it can tell the user a more real performance result of the model we build.
|
||||
{% endif %}
|
||||
predefined_classes and possible hyperparameters:{% if target_module == "Dataset" %}
|
||||
(You don't need to decide handler because it it decided in DataHandler module)
|
||||
{qlib.data.dataset}-{DatasetH}:
|
||||
{segments}
|
||||
{fetch_kwargs}
|
||||
target component: Dataset
|
||||
{% elif target_module == "DataHandler" %}
|
||||
{qlib.contrib.data.handler}-{Alpha360}:
|
||||
{instruments}
|
||||
{start_time}
|
||||
{end_time}
|
||||
{freq}
|
||||
{infer_processors}
|
||||
{learn_processors}
|
||||
{fit_start_time}
|
||||
{fit_end_time}
|
||||
{filter_pipe}
|
||||
{inst_processors}
|
||||
{data_loader}
|
||||
{label}
|
||||
target component: DataHandler
|
||||
{% elif target_module == "Model" %}
|
||||
{qlib.contrib.model.pytorch_lstm}-{LSTM}
|
||||
{d_feat}
|
||||
{hidden_size}
|
||||
{num_layers}
|
||||
{dropout}
|
||||
{n_epochs}
|
||||
{lr}
|
||||
{metric}
|
||||
{batch_size}
|
||||
{early_stop}
|
||||
{loss}
|
||||
{optimizer}
|
||||
{GPU}
|
||||
{seed}
|
||||
target component: Model
|
||||
{% elif target_module == "Record" %}
|
||||
{qlib.workflow.record_temp}-{SignalRecord}:
|
||||
{model}
|
||||
{dataset}
|
||||
{recorder}
|
||||
{workspace}
|
||||
{qlib.workflow.record_temp}-{SigAnaRecord}:
|
||||
{recorder}
|
||||
{ana_long_short}
|
||||
{ann_scaler}
|
||||
{label_col}
|
||||
{skip_existing}
|
||||
target component: Record
|
||||
{% elif target_module == "Strategy" %}
|
||||
{qlib.contrib.strategy}-{TopkDropoutStrategy}:
|
||||
{topk}
|
||||
{n_drop}
|
||||
{method_sell}
|
||||
{method_buy}
|
||||
{hold_thresh}
|
||||
{only_tradable}
|
||||
{forbid_all_trade_at_limit}
|
||||
target component: Strategy
|
||||
{% elif target_module == "Backtest" %}
|
||||
Default:
|
||||
{start_time}
|
||||
{end_time}
|
||||
{strategy}
|
||||
{executor}
|
||||
{benchmark}
|
||||
{account}
|
||||
{exchange_kwargs}
|
||||
{pos_type}
|
||||
target component: Backtest
|
||||
{% endif %}
|
||||
Example output:
|
||||
{{target_component_example_output}}
|
||||
Hyperparameters:
|
||||
{% if target_module == "Dataset" %}
|
||||
{qlib.data.dataset}-{DatasetH}:
|
||||
{segments}:{"train":["2008-01-01", "2014-12-31"],"valid": ["2015-01-01", "2016-12-31"],"test": ["2017-01-01", "2020-08-01"]}
|
||||
{fetch_kwargs}:Default
|
||||
Reason: I chose these hyperparameters to provide a robust long-term investment strategy by dividing the China A stock market data into train, valid, and test segments, while using default settings for fetching data in Qlib's DatasetH.
|
||||
Improve suggestion: To further improve the model's performance, you can experiment with different segment lengths for train, valid, and test sets, and explore custom fetch_kwargs options to include additional features or modify data preprocessing steps in Qlib's DatasetH.
|
||||
{% elif target_module == "DataHandler" %}
|
||||
{qlib.contrib.data.handler}-{Alpha360}:
|
||||
{instruments}:csi300
|
||||
{start_time}:2008-01-01
|
||||
{end_time}:2020-08-01
|
||||
{freq}:Default
|
||||
{infer_processors}:[{"class":"RobustZScoreNorm","kwargs":{"fields_group":"feature", "clip_outlier":"true"}}, {"class":"Fillna","kwargs":{"fields_group":"feature"}}}]
|
||||
{learn_processors}:[{"class":"DropnaLabel"}, {"class":"CSRankNorm","kwargs":{"fields_group":"label"}}}]
|
||||
{fit_start_time}:2008-01-01
|
||||
{fit_end_time}:2014-12-31
|
||||
{filter_pipe}:Default
|
||||
{inst_processors}:Default
|
||||
{data_loader}:Default
|
||||
{label}:["Ref($close, -2) / Ref($close, -1) - 1"]
|
||||
Reason: The hyperparameters are chosen to focus on the China A stock market, provide a long-term historical dataset, ensure a robust training period, and align with your longer-term return focus, thus creating a solid starting point for your low turnover quant investment strategy.
|
||||
Improve suggestion: To improve DataHandler performance, consider using more recent data, experimenting with different frequencies, customizing learn_processors and infer_processors, exploring feature selection techniques, and trying different labels. Be aware that these suggestions may require additional resources and experimentation to optimize your strategy.
|
||||
{% elif target_module == "Model" %}
|
||||
{qlib.contrib.model.pytorch_lstm}-{LSTM}
|
||||
{d_feat}:6
|
||||
{hidden_size}:64
|
||||
{num_layers}:2
|
||||
{dropout}:0.0
|
||||
{n_epochs}:200
|
||||
{lr}:0.001
|
||||
{metric}:loss
|
||||
{batch_size}:800
|
||||
{early_stop}:20
|
||||
{loss}:mse
|
||||
{optimizer}:Default
|
||||
{GPU}:Default
|
||||
{seed}:Default
|
||||
Reason: I chose these hyperparameters based on your requirement of a big LSTM model for a low turnover quant investment strategy focusing on long-term returns in the China A-stock market. The hidden_size is set to 128 to create a larger LSTM model, while the num_layers is set to 2 to add complexity. The dropout is set to 0.0 to avoid any regularization, as you didn't mention any concerns about overfitting. The learning rate (lr) is set to 0.001, which is a standard value for training deep learning models. The number of epochs (n_epochs) is set to 200, and the early stopping (early_stop) is set to 20, which will help in preventing overfitting and save training time. The batch_size is set to 800, which is a reasonable size for training the model. The loss function is set to mean squared error (mse) as it is a common choice for regression tasks. The optimizer, GPU, and seed are set to their default values as no specific requirements were provided.
|
||||
Improve suggestion: To further improve the performance of the LSTM model, you can consider tuning the following hyperparameters: hidden_size, num_layers, dropout, learning rate (lr), and batch_size. You can try increasing the hidden_size and num_layers to make the model more expressive, but be cautious of overfitting. Introducing dropout (e.g., 0.1 to 0.5) can help with regularization and reduce overfitting. Adjusting the learning rate (e.g., trying values between 0.0001 and 0.01) can help find the optimal balance between fast convergence and stable training. Lastly, experimenting with different batch sizes (e.g., 256, 512, or 1024) can impact the model's performance and training speed. You can use techniques like grid search or random search to systematically explore these hyperparameter combinations and select the best performing configuration.
|
||||
{% elif target_module == "Record" %}
|
||||
{qlib.workflow.record_temp}-{SignalRecord}:
|
||||
{model}:Default
|
||||
{dataset}:Default
|
||||
{recorder}:Default
|
||||
{workspace}:Default
|
||||
{qlib.workflow.record_temp}-{SigAnaRecord}:
|
||||
{recorder}:Default
|
||||
{ana_long_short}:False
|
||||
{ann_scaler}:252
|
||||
{label_col}:Default
|
||||
{skip_existing}:Default
|
||||
Reason: For {qlib.workflow.record_temp}-{SignalRecord}, the default settings should work well with the user's requirements, so no changes are needed. For {qlib.workflow.record_temp}-{SigAnaRecord}, I set {ana_long_short} to False because the user wants to focus more on long-term returns. The {ann_scaler} is set to 252, which is the number of trading days in a year, to analyze the annualized return.
|
||||
Improve suggestion: To finetune the hyperparameters, the user can try different values for {ann_scaler} if they want to analyze the return over different time horizons. They can also experiment with different {label_col} settings if they want to analyze different aspects of the strategy's performance.
|
||||
{% elif target_module == "Strategy" %}
|
||||
{qlib.contrib.strategy}-{TopkDropoutStrategy}:
|
||||
{topk}:50
|
||||
{n_drop}:5
|
||||
{method_sell}:Default
|
||||
{method_buy}:Default
|
||||
{hold_thresh}:Default
|
||||
{only_tradable}:Default
|
||||
{forbid_all_trade_at_limit}:Default
|
||||
Reason: The user wants a low turnover strategy that focuses on long-term returns. By choosing TopkDropoutStrategy and setting topk to 50, we select the top 50 stocks based on the model predictions. The n_drop parameter is set to 5, which means that we will drop the bottom 5 stocks from the top 50 when rebalancing the portfolio. This will help in reducing the turnover and focusing on long-term returns.
|
||||
Improve suggestion: If the user wants to further fine-tune the hyperparameters, they can experiment with different values for topk and n_drop to see how it affects the strategy's performance. They can also try other methods for method_sell and method_buy to see if it improves the results. Additionally, the user can consider adjusting the hold_thresh parameter to control the holding threshold for stocks in the portfolio.
|
||||
{% elif target_module == "Backtest" %}
|
||||
Default:
|
||||
{start_time}:2017-01-01
|
||||
{end_time}:2020-08-01
|
||||
{strategy}:Default
|
||||
{executor}:Default
|
||||
{benchmark}:SH000300
|
||||
{account}:100000000
|
||||
{exchange_kwargs}:{ "limit_threshold": 0.095, "deal_price": "close", "open_cost": 0.0005, "close_cost": 0.0015, "min_cost": 5}
|
||||
{pos_type}:Default
|
||||
Reason: The start_time and end_time are set to 2017-01-01 and 2020-08-01 to ensure a sufficient time range for the model to learn and test. The benchmark is set to SH000300, which represents the China A-share stock market, to match the user's requirement. The account is set to 100000000 to provide enough initial capital for the investment strategy. The exchange_kwargs are set to control the transaction costs and price limits to simulate a realistic trading environment.
|
||||
Improve suggestion: Adjust the start_time and end_time according to the available data and the desired testing period, and fine-tune the transaction costs in exchange_kwargs to match the actual trading environment.
|
||||
{% endif %}
|
||||
|
||||
ConfigActionTask_user : |-
|
||||
user requirement: {% raw %}{{user_requirement}}{% endraw %}
|
||||
HyperparameterActionTask_user : |-
|
||||
user requirement: {{user_requirement}}
|
||||
user plan:
|
||||
- {{target_component}}: {% raw %}({{decision}}) {{plan}}{% endraw %}
|
||||
{{target_component_plan}}
|
||||
|
||||
predefined_classes and possible hyperparameters:{%if target_component == "Dataset"%}
|
||||
(You don't need to decide handler because it it decided in DataHandler module){% endif %}{%if target_component == "Backtest"%}Default:
|
||||
{% else %}
|
||||
{%for module_path, class_name, params in target_component_classes_and_hyperparameters%}
|
||||
{% raw %}{{% endraw %}{{module_path}}{% raw %}}{% endraw %}-{% raw %}{{% endraw %}{{class_name}}{% raw %}}{% endraw %}:
|
||||
{%for param in params%}{% raw %}{{% endraw %}{{param}}{% raw %}}{% endraw %}
|
||||
{% endfor %}{% endfor %}{% endif %}
|
||||
target component: {{target_component}}
|
||||
|
||||
ConfigActionTask_system: |-
|
||||
Your task is to write the YAML config in Qlib following user's intention on target component of Qlib(Dataset, DataHandler, Model, Record, Strategy, Backtest).
|
||||
|
||||
The predefined class in the target Qlib module can be listed in format of {module_path}-{class name}:
|
||||
{% if target_module == "Dataset" %}
|
||||
Dataset: {qlib.data.dataset}-{DatasetH}, {qlib.contrib.data.dataset}-{MTSDatasetH}
|
||||
{% elif target_module == "DataHandler" %}
|
||||
DataHandler: {qlib.contrib.data.handler}-{Alpha158}, {qlib.contrib.data.handler}-{Alpha158vwap}, {qlib.contrib.data.handler}-{Alpha360}, {qlib.contrib.data.handler}-{Alpha360vwap}, {qlib.data.dataset.loader}-{QlibDataLoader}
|
||||
{% elif target_module == "Model" %}
|
||||
Model: {qlib.contrib.model.catboost_model}-{CatBoostModel}, {qlib.contrib.model.double_ensemble}-{DoubleEnsembleModel}, {qlib.contrib.model.gbdt}-{LGBModel}, {qlib.contrib.model.highfreq_gdbt_model}-{HFLGBModel}, {qlib.contrib.model.linear}-{LinearModel}, {qlib.contrib.model.pytorch_adarnn}-{AdaRNNModel}, {qlib.contrib.model.pytorch_add}-{ADD}, {qlib.contrib.model.pytorch_alstm_ts}-{ALSTM}, {qlib.contrib.model.pytorch_alstm}-{ALSTM}, {qlib.contrib.model.pytorch_gats}-{GATs}, {qlib.contrib.model.pytorch_gats_ts}-{GATs}, {qlib.contrib.model.pytorch_gru}-{GRU}, {qlib.contrib.model.pytorch_gru_ts}-{GRU}, {qlib.contrib.model.pytorch_hist}-{HIST}, {qlib.contrib.model.pytorch_igmtf}-{IGMTF}, {qlib.contrib.model.pytorch_localformer}-{LocalformerModel}, {qlib.contrib.model.pytorch_localformer_ts}-{LocalformerModel}, {qlib.contrib.model.pytorch_lstm}-{LSTM}, {qlib.contrib.model.pytorch_lstm_ts}-{LSTM}, {qlib.contrib.model.pytorch_nn}-{DNNModelPytorch}, {qlib.contrib.model.pytorch_sfm}-{SFM}, {qlib.contrib.model.pytorch_tabnet}-{TabnetModel}, {qlib.contrib.model.pytorch_tcn_ts}-{TCN}, {qlib.contrib.model.pytorch_tcn}-{TCN}, {qlib.contrib.model.pytorch_tcts.}-{TCTS}, {qlib.contrib.model.pytorch_tra}-{TRA}, {qlib.contrib.model.pytorch_transformer}-{TransformerModel}, {qlib.contrib.model.pytorch_transformer_ts}-{TransformerModel}, {qlib.contrib.model.xgboost}-{XGBModel}
|
||||
{% elif target_module == "Record" %}
|
||||
Record: {qlib.workflow.record_temp}-{SignalRecord}, {qlib.workflow.record_temp}-{SigAnaRecord},
|
||||
{% elif target_module == "Strategy" %}
|
||||
Strategy: {qlib.contrib.strategy}-{TopkDropoutStrategy}, {qlib.contrib.strategy}-{WeightStrategyBase}, {qlib.contrib.strategy}-{EnhancedIndexingStrategy}, {qlib.contrib.strategy}-{TWAPStrategy}, {qlib.contrib.strategy}-{SBBStrategyBase}, {qlib.contrib.strategy}-{SBBStrategyEMA}, {qlib.contrib.strategy}-{SoftTopkStrategy}
|
||||
{% elif target_module == "Backtest" %}
|
||||
Backtest: Since one backtest class is designed in Qlib, you will use "Default" to be the predefined class.
|
||||
{% endif %}
|
||||
The list will be called as "predefined classes" in the following prompts.
|
||||
{% if target_module != "Backtest" %}
|
||||
You have chosen Default backtest module.
|
||||
{% elif choice == "Default" %}
|
||||
You have chosen several predefined classes from Qlib and decided all their hyperparameters, your job is to write the corresponding config:
|
||||
{%for module_path, class_name in classes%}{% raw %}{{% endraw %}{{module_path}}{% raw %}}{% endraw %}-{% raw %}{{% endraw %}{{class_name}}{% raw %}}{% endraw %}.{% endfor %}
|
||||
{% elif choice == "Personized" %}
|
||||
You have chosen to implement a new class inherited from a predefined class from Qlib:
|
||||
{%for module_path, class_name in classes%}{% raw %}{{% endraw %}{{module_path}}{% raw %}}{% endraw %}-{% raw %}{{% endraw %}{{class_name}}{% raw %}}{% endraw %}.{% endfor %} and you have decided all the hyperparameters.
|
||||
{% endif %}
|
||||
|
||||
Default in user's hyperparameter means using default value in Qlib code. So always remember to avoid puting them in the config and delete this key in yaml string!!!
|
||||
You only output the target component part of the config, Don't output all the config file!!!
|
||||
|
||||
User will provide:
|
||||
1. the requirement
|
||||
2. user's plan on the target module
|
||||
3. user's choice of predefined class
|
||||
4. each predefined class's hyperparameter to initialize the class
|
||||
|
||||
You will response the YAML config with no explanation and interaction.
|
||||
|
||||
Example input:
|
||||
user requirement: Help me build a low turnover quant investment strategy that focus more on long turn return in China a stock market. I want to use a big LSTM model and add several MLP layer before the head.
|
||||
user plan:{% if target_module == "Dataset" %}
|
||||
Dataset: (Default) {qlib.data.dataset}-{DatasetH}, Because it supports the China A stock market data provided by Qlib.
|
||||
{% elif target_module == "DataHandler" %}
|
||||
DataHandler: (Default) {qlib.contrib.data.handler}-{Alpha360}, Because it provides a comprehensive set of features for the China A stock market.
|
||||
{% elif target_module == "Model" %}
|
||||
Model: (Default) {qlib.contrib.model.gbdt}-{LGBModel}, Because it uses the LightGBM model requested by the user.
|
||||
{% elif target_module == "Record" %}
|
||||
Record: (Default) {qlib.workflow.record_temp}-{SignalRecord}{qlib.workflow.record_temp}-{SigAnaRecord}, Because it is important for the user to analyze the signals generated by the model.
|
||||
{% elif target_module == "Strategy" %}
|
||||
Strategy: (Default) {qlib.contrib.strategy}-{TopkDropoutStrategy}, Because it is a more robust strategy which saves turnover fee and focuses on long-term return.
|
||||
{% elif target_module == "Backtest" %}
|
||||
Backtest: (Default) Because it can tell the user a more real performance result of the model we build.
|
||||
{% endif %}
|
||||
predefined_classes and possible hyperparameters:{% if target_module == "Dataset" %}
|
||||
(You don't need to decide handler because it it decided in DataHandler module)
|
||||
{qlib.data.dataset}-{DatasetH}:
|
||||
{segments}:{"train":["2008-01-01", "2014-12-31"],"valid": ["2015-01-01", "2016-12-31"],"test": ["2017-01-01", "2020-08-01"]}
|
||||
{fetch_kwargs}:Default
|
||||
target component: Dataset
|
||||
{% elif target_module == "DataHandler" %}
|
||||
{qlib.contrib.data.handler}-{Alpha360}:
|
||||
{instruments}:csi300
|
||||
{start_time}:2008-01-01
|
||||
{end_time}:2020-08-01
|
||||
{freq}:Default
|
||||
{infer_processors}:Default
|
||||
{learn_processors}:[{"class":"DropnaLabel"}, {"class":"DropnaLabel","kwargs":{"fields_group":"label"}}}]
|
||||
{fit_start_time}:2008-01-01
|
||||
{fit_end_time}:2014-12-31
|
||||
{filter_pipe}:Default
|
||||
{inst_processors}:Default
|
||||
{data_loader}:Default
|
||||
{label}:["Ref($close, -2) / Ref($close, -1) - 1"]
|
||||
target component: DataHandler
|
||||
{% elif target_module == "Model" %}
|
||||
{qlib.contrib.model.pytorch_lstm}-{LSTM}
|
||||
{d_feat}:6
|
||||
{hidden_size}:64
|
||||
{num_layers}:2
|
||||
{dropout}:0.0
|
||||
{n_epochs}:200
|
||||
{lr}:0.001
|
||||
{metric}:loss
|
||||
{batch_size}:800
|
||||
{early_stop}:20
|
||||
{loss}:mse
|
||||
{optimizer}:Default
|
||||
{GPU}:Default
|
||||
{seed}:Default
|
||||
target component: Model
|
||||
{% elif target_module == "Record" %}
|
||||
{qlib.workflow.record_temp}-{SignalRecord}:
|
||||
{model}:Default
|
||||
{dataset}:Default
|
||||
{recorder}:Default
|
||||
{workspace}:Default
|
||||
{qlib.workflow.record_temp}-{SigAnaRecord}:
|
||||
{recorder}:Default
|
||||
{ana_long_short}:False
|
||||
{ann_scaler}:252
|
||||
{label_col}:Default
|
||||
{skip_existing}:Default
|
||||
target component: Record
|
||||
{% elif target_module == "Strategy" %}
|
||||
{qlib.contrib.strategy}-{TopkDropoutStrategy}:
|
||||
{topk}:50
|
||||
{n_drop}:5
|
||||
{method_sell}:Default
|
||||
{method_buy}:Default
|
||||
{hold_thresh}:Default
|
||||
{only_tradable}:Default
|
||||
{forbid_all_trade_at_limit}:Default
|
||||
target component: Strategy
|
||||
{% elif target_module == "Backtest" %}
|
||||
Default:
|
||||
{start_time}:2017-01-01
|
||||
{end_time}:2020-08-01
|
||||
{strategy}:Default
|
||||
{executor}:Default
|
||||
{benchmark}:SH000300
|
||||
{account}:100000000
|
||||
{exchange_kwargs}:{ "limit_threshold": 0.095, "deal_price": "close", "open_cost": 0.0005, "close_cost": 0.0015, "min_cost": 5}
|
||||
{pos_type}:Default
|
||||
target component: Backtest
|
||||
{% endif %}
|
||||
Example output:
|
||||
"""yaml{% if target_module == "Dataset" %}
|
||||
dataset:
|
||||
class: DatasetH
|
||||
module_path: qlib.data.dataset
|
||||
kwargs:
|
||||
segments:
|
||||
train: [2008-01-01, 2014-12-31]
|
||||
valid: [2015-01-01, 2016-12-31]
|
||||
test: [2017-01-01, 2020-08-01]
|
||||
{% elif target_module == "DataHandler" %}
|
||||
handler:
|
||||
class: Alpha360
|
||||
module_path: qlib.contrib.data.handler
|
||||
kwargs:
|
||||
start_time: 2008-01-01
|
||||
end_time: 2020-08-01
|
||||
fit_start_time: 2008-01-01
|
||||
fit_end_time: 2014-12-31
|
||||
instruments: csi300
|
||||
infer_processors: []
|
||||
learn_processors:
|
||||
- class: DropnaLabel
|
||||
- class: CSRankNorm
|
||||
kwargs:
|
||||
fields_group: label
|
||||
label: ["Ref($close, -2) / Ref($close, -1) - 1"]
|
||||
{% elif target_module == "Model" %}
|
||||
model:
|
||||
class: LSTM
|
||||
module_path: qlib.contrib.model.pytorch_lstm
|
||||
kwargs:
|
||||
d_feat: 6
|
||||
hidden_size: 64
|
||||
num_layers: 2
|
||||
dropout: 0.0
|
||||
n_epochs: 200
|
||||
lr: 1e-3
|
||||
early_stop: 20
|
||||
batch_size: 800
|
||||
metric: loss
|
||||
loss: mse
|
||||
GPU: 0
|
||||
{% elif target_module == "Record" %}
|
||||
record:
|
||||
- class: SignalRecord
|
||||
module_path: qlib.workflow.record_temp
|
||||
- class: SigAnaRecord
|
||||
module_path: qlib.workflow.record_temp
|
||||
kwargs:
|
||||
ana_long_short: False
|
||||
ann_scaler: 252
|
||||
{% elif target_module == "Strategy" %}
|
||||
strategy:
|
||||
class: TopkDropoutStrategy
|
||||
module_path: qlib.contrib.strategy
|
||||
kwargs:
|
||||
topk: 50
|
||||
n_drop: 5
|
||||
{% elif target_module == "Backtest" %}
|
||||
backtest:
|
||||
start_time: 2017-01-01
|
||||
end_time: 2020-08-01
|
||||
account: 100000000
|
||||
benchmark: SH000300
|
||||
exchange_kwargs:
|
||||
limit_threshold: 0.095
|
||||
deal_price: close
|
||||
open_cost: 0.0005
|
||||
close_cost: 0.0015
|
||||
min_cost: 5
|
||||
{% endif %}"""
|
||||
|
||||
ConfigActionTask_user: |-
|
||||
user requirement: {{user_requirement}}
|
||||
user plan:
|
||||
{{target_component_plan}}
|
||||
predefined_classes and possible hyperparameters:{%if target_component == "Dataset"%}
|
||||
(You don't need to decide handler because it it decided in DataHandler module){% endif %}{%if target_component == "Backtest"%}Default:{% endif %}{{target_component_hyperparameters}}target component: {{target_component}}
|
||||
|
||||
ImplementActionTask_system : |-
|
||||
Your task is to write python code and give some reasonable explanation. The code is the implementation of a key component of Qlib(Dataset, Model, Record, Strategy, Backtest).
|
||||
|
||||
@@ -199,331 +610,3 @@ BackForwardTask_system : |-
|
||||
BackForwardTask_user : |-
|
||||
Here is the final summary: '{{summary}}'
|
||||
Tasks I have run are: {{task_finished}}, {{task}}'s system prompt is: {{system}}. User's intention is: {{user_prompt}}. you will adjust it to:
|
||||
|
||||
mods:
|
||||
ConfigActionTask_system:
|
||||
Dataset:
|
||||
target_component : |-
|
||||
Dataset
|
||||
target_component_desc : |-
|
||||
The Dataset config always contains a class name and the module_path. You should make sure the class exists in module_path if user plans to use default module in qlib. You should pick a reasonable segments to verify the power of the strategy.
|
||||
|
||||
You should not write the handler part since it will be determined independently.
|
||||
target_component_example_input : |-
|
||||
- Dataset: (Personized) I will implement a CustomDataset inherited from DatasetH in qlib.data.dataset and merge the user-provided CSV data with the existing Qlib data. Because this will allow the user to leverage both their custom data and Qlib's data for the China A stock market.
|
||||
target_component_example_output : |-
|
||||
Config:
|
||||
```yaml
|
||||
dataset:
|
||||
class: CustomDataset
|
||||
module_path: your_custom_module_path
|
||||
kwargs:
|
||||
train_start: 2008-01-01
|
||||
train_end: 2014-12-31
|
||||
valid_start: 2015-01-01
|
||||
valid_end: 2016-12-31
|
||||
test_start: 2017-01-01
|
||||
test_end: 2020-12-31
|
||||
freq: D
|
||||
max_len: 200
|
||||
benchmark: SH000300
|
||||
```
|
||||
Reason: I choose the segments above because they can help to verify the power of the strategy. The train, valid, and test segments are separated in different periods to avoid overfitting and make sure the model can capture long-term patterns in the data.
|
||||
|
||||
Improve suggestion: You can try to adjust the train, valid, and test segments to see how the model performs in different periods. Also, you can try different values for max_len and freq to see if it affects the performance of the strategy.
|
||||
|
||||
DataHandler:
|
||||
target_component : |-
|
||||
DataHandler
|
||||
target_component_desc : |-
|
||||
The DataHandler config always contains a class name and the module_path. You should make sure the class exists in module_path if user plans to use default module in qlib. You should pick a reasonable model class meet the user's requirements.
|
||||
|
||||
If you decide to use Alpha158 or Alpha360, you can modify the dataloader in the kwargs of the datahandler to choose your own feature and label using expressions and change the preprocess functions.
|
||||
target_component_example_input : |-
|
||||
- DataHandler: (Personized) I will implement a CustomDataHandler inherited from Alpha360 in qlib.contrib.data.handler to handle the merged dataset from the custom dataset. Because it is necessary to handle the combined data from Qlib's Alpha360 and the user's CSV file.
|
||||
target_component_example_output : |-
|
||||
Config:
|
||||
```yaml
|
||||
handler:
|
||||
class: CustomDataHandler
|
||||
module_path: your_custom_module_path
|
||||
kwargs:
|
||||
instruments: all
|
||||
start_time: 2008-01-01
|
||||
end_time: 2020-12-31
|
||||
freq: D
|
||||
infer_processors:
|
||||
- class: FilterCol
|
||||
kwargs:
|
||||
fields: "!{REMOVE}"
|
||||
- class: DropnaCol
|
||||
kwargs:
|
||||
fields: "!{REMOVE}"
|
||||
- class: Normalize
|
||||
kwargs:
|
||||
fields: "!{REMOVE}"
|
||||
features:
|
||||
- class: ExpressionD
|
||||
kwargs:
|
||||
expr: "MA({close}, 200)"
|
||||
- class: ExpressionD
|
||||
kwargs:
|
||||
expr: "EMA({close}, 100)"
|
||||
- class: ExpressionD
|
||||
kwargs:
|
||||
expr: "BBANDS({close}, 50)"
|
||||
- class: ExpressionD
|
||||
kwargs:
|
||||
expr: "ROC({close}, 252)"
|
||||
- class: ExpressionD
|
||||
kwargs:
|
||||
expr: "AD({high}, {low}, {close}, {volume})"
|
||||
- class: ExpressionD
|
||||
kwargs:
|
||||
expr: "OBV({close}, {volume})"
|
||||
- class: ExpressionD
|
||||
kwargs:
|
||||
expr: "MFI({high}, {low}, {close}, {volume}, 14)"
|
||||
labels:
|
||||
- class: ExpressionD
|
||||
kwargs:
|
||||
expr: "CSRank({close})"
|
||||
```
|
||||
Reason: The selected features include long-term moving averages (MA200 and EMA100), Bollinger Bands with a 50-day window, rate of change (ROC) for the past 252 trading days (approximately one year), accumulation/distribution line (AD), on-balance volume (OBV), and money flow index (MFI) with a 14-day window. These features are more focused on long-term trends and could potentially help achieve long-term returns.
|
||||
|
||||
Improve suggestion: You can try to add more features or adjust the parameters of the existing features to see if it improves the performance of the model. You can also experiment with different labels, such as using the future return or the cross-sectional rank of the future return as the label.
|
||||
|
||||
Model:
|
||||
target_component : |-
|
||||
Model
|
||||
target_component_desc : |-
|
||||
The Dataset config always contains a class name and the module_path. You should make sure the class exists in module_path if user plans to use default module in qlib. You should pick a reasonable model class meet the user's requirements.
|
||||
|
||||
You should also pick the kwargs carefully to try to get the model with best performance.
|
||||
target_component_example_input : |-
|
||||
- Model: (Default) I will use LGBModel in qlib.contrib.model.gbdt for the low turnover strategy. Because it is a popular and efficient model for quant investment strategies and can capture long-term patterns in the data.
|
||||
target_component_example_output : |-
|
||||
Config:
|
||||
```yaml
|
||||
model:
|
||||
class: LGBModel
|
||||
module_path: qlib.contrib.model.gbdt
|
||||
kwargs:
|
||||
loss: mse
|
||||
colsample_bytree: 0.8879
|
||||
learning_rate: 0.2
|
||||
subsample: 0.8789
|
||||
lambda_l1: 205.6999
|
||||
lambda_l2: 580.9768
|
||||
max_depth: 8
|
||||
num_leaves: 210
|
||||
num_threads: 20
|
||||
```
|
||||
Reason: I choose the hyperparameters above because they are the default hyperparameters in Qlib and they are more robust than other hyperparameters.
|
||||
|
||||
Improve suggestion: You can try to tune the num_leaves in range [100, 300], max_depth in [5, 10], learning_rate in [0.01, 1] and other hyperparameters in the config. Since you're trying to get a long tern return, if you have enough computation resource, you can try to use a larger num_leaves and max_depth and a smaller learning_rate.
|
||||
|
||||
Record:
|
||||
target_component : |-
|
||||
Record
|
||||
target_component_desc : |-
|
||||
The Record config contains several class name and corresponding module_path. You should make sure the class exists in module_path.
|
||||
target_component_example_input : |-
|
||||
- Record: (Default) I will use SignalRecord in qlib.workflow.record_temp and SigAnaRecord in qlib.workflow.record_temp to save all the signals and the analysis results. Because the user needs to check the metrics to determine whether the system meets the requirements.
|
||||
target_component_example_output : |-
|
||||
Config:
|
||||
```yaml
|
||||
record:
|
||||
- class: SignalRecord
|
||||
module_path: qlib.workflow.record_temp
|
||||
- class: SigAnaRecord
|
||||
module_path: qlib.workflow.record_temp
|
||||
```
|
||||
Reason: I choose SignalRecord and SigAnaRecord because they can save all the signals and the analysis results, which will help the user check the metrics and determine whether the system meets the requirements.
|
||||
Improve suggestion: The default record classes should be sufficient for your needs. If you need additional information or metrics, you can consider implementing your own custom record class.
|
||||
|
||||
Strategy:
|
||||
target_component : |-
|
||||
Strategy
|
||||
target_component_desc : |-
|
||||
The Strategy config always contains a class name and corresponding module_path. You should make sure the class exists in module_path.
|
||||
|
||||
You should pick reasonable kwargs to meet the user's requirements.
|
||||
target_component_example_input : |-
|
||||
- Strategy: (Default) I will use TopkDropoutStrategy in qlib.contrib.strategy. Because it is a more robust strategy which saves turnover fee and focuses on long-term return.
|
||||
target_component_example_output : |-
|
||||
Config:
|
||||
```yaml
|
||||
strategy:
|
||||
class: TopkDropoutStrategy
|
||||
module_path: qlib.contrib.strategy
|
||||
kwargs:
|
||||
topk: 50
|
||||
n_drop: 5
|
||||
```
|
||||
Reason: I choose the topk and n_drop parameters because they are reasonable default values for a low turnover strategy that focuses on long-term returns. Topk selects the top 50 stocks with the highest scores, and n_drop drops the bottom 5 stocks from the existing portfolio to reduce turnover.
|
||||
Improve suggestion: You can try to adjust the topk parameter in the range [30, 100] and n_drop parameter in the range [1, 10] to find the optimal balance between portfolio diversification and turnover.
|
||||
|
||||
Backtest:
|
||||
target_component : |-
|
||||
Backtest
|
||||
target_component_desc : |-
|
||||
You should pick reasonable parameters to meet the user's requirements. You should always provide the config even user never mentioned this component.
|
||||
target_component_example_input : |-
|
||||
- Backtest: (Default) I will use the default backtest module in Qlib. Because it can tell the user a more real performance result of the model we build.
|
||||
target_component_example_output : |-
|
||||
Config:
|
||||
```yaml
|
||||
backtest:
|
||||
start_time: 2017-01-01
|
||||
end_time: 2020-12-31
|
||||
account: 1000000
|
||||
benchmark: SH000300
|
||||
exchange_kwargs:
|
||||
freq: day
|
||||
limit_threshold: 0.095
|
||||
deal_price: close
|
||||
open_cost: 0.0005
|
||||
close_cost: 0.0015
|
||||
min_cost: 5
|
||||
```
|
||||
Reason: I choose the backtest parameters above because they are suitable for a low turnover strategy focusing on long-term returns in the China A stock market. The start and end times are set to cover a 4-year period, which is reasonable for a long-term strategy. The account value is set to 1,000,000 as a starting point, and the benchmark is set to SH000300, which represents the China A stock market.
|
||||
Improve suggestion: You can try different time ranges for the backtest to evaluate the performance of the strategy in different market conditions. Also, you can adjust the costs (open_cost, close_cost, and min_cost) to better reflect the actual trading costs in the China A stock market.
|
||||
|
||||
ConfigActionTask_user:
|
||||
Dataset:
|
||||
target_component : |-
|
||||
Dataset
|
||||
DataHandler:
|
||||
target_component : |-
|
||||
DataHandler
|
||||
Model:
|
||||
target_component : |-
|
||||
Model
|
||||
Record:
|
||||
target_component : |-
|
||||
Record
|
||||
Strategy:
|
||||
target_component : |-
|
||||
Strategy
|
||||
Backtest:
|
||||
target_component : |-
|
||||
Backtest
|
||||
|
||||
ImplementActionTask_system:
|
||||
Dataset:
|
||||
target_component : |-
|
||||
Dataset
|
||||
target_component_desc : |-
|
||||
You should Inherit the class to DatasetH and try to maintain the API for other modules to call directly.
|
||||
target_component_example_output : |-
|
||||
Code:
|
||||
```python
|
||||
import pandas as pd
|
||||
from qlib.data.dataset import DatasetH
|
||||
|
||||
class CustomDataset(DatasetH):
|
||||
def __init__(self, handler, csv_path):
|
||||
super().__init__(handler)
|
||||
self.csv_data = pd.read_csv(csv_path)
|
||||
|
||||
def prepare(self, *args, **kwargs):
|
||||
super().prepare(*args, **kwargs)
|
||||
self._data = self._data.merge(self.csv_data, on=["date", "instrument"], how="left")
|
||||
```
|
||||
Explanation:
|
||||
In this implementation, the CustomDataset class inherits from DatasetH and takes the handler and csv_path as arguments. The prepare method is overridden to merge the csv data with Qlib data after calling the parent's prepare method.
|
||||
Modified config:
|
||||
```yaml
|
||||
dataset:
|
||||
class: CustomDataset
|
||||
module_path: custom_dataset
|
||||
```
|
||||
|
||||
DataHandler:
|
||||
target_component : |-
|
||||
DataHandler
|
||||
target_component_desc : |-
|
||||
You should Inherit the class to Alpha360, Alpha158 or DataHandlerLP and try to maintain the API for other modules to call directly. You can change some of the functions to meet user's requirement, but you should not change the API.
|
||||
target_component_example_output : |-
|
||||
Code:
|
||||
```python
|
||||
import pandas as pd
|
||||
from qlib.contrib.data.handler import Alpha360
|
||||
|
||||
class CustomDataHandler(Alpha360):
|
||||
def __init__(self, csv_path, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.csv_data = pd.read_csv(csv_path)
|
||||
|
||||
def load_all(self):
|
||||
qlib_data = super().load_all()
|
||||
merged_data = qlib_data.merge(self.csv_data, on=["date", "instrument"], how="left")
|
||||
return merged_data
|
||||
```
|
||||
Explanation:
|
||||
The CustomDataHandler class inherits from Alpha360 and merges the CSV data with Qlib data. It overrides the load_all method to perform the merging.
|
||||
Modified config:
|
||||
```yaml
|
||||
handler:
|
||||
class: CustomDataHandler
|
||||
module_path: custom_data_handler
|
||||
kwargs:
|
||||
csv_path: path/to/your/csv/data
|
||||
```
|
||||
|
||||
Model:
|
||||
target_component : |-
|
||||
Model
|
||||
target_component_desc : |-
|
||||
Model component not only contain a model itself, it is a class inherited from BaseModel from qlib.model containing apis of fit and predict.
|
||||
target_component_example_output : |-
|
||||
Code:
|
||||
```python
|
||||
import torch.nn as nn
|
||||
from qlib.contrib.model.pytorch_transformer import TransformerModel
|
||||
|
||||
|
||||
class CustomTransformerModel(TransformerModel):
|
||||
def __init__(self, d_feat, n_head=8, num_layers=6, d_model=64, d_ff=2048, dropout=0.1, **kwargs):
|
||||
super().__init__(d_feat, n_head, num_layers, d_model, d_ff, dropout)
|
||||
|
||||
# Add the additional MLP layers before the head
|
||||
self.model = nn.Sequential(
|
||||
self.model,
|
||||
nn.Linear(d_model, d_model),
|
||||
nn.ReLU(),
|
||||
nn.Linear(d_model, d_model),
|
||||
nn.ReLU(),
|
||||
nn.Linear(d_model, d_model),
|
||||
nn.ReLU(),
|
||||
nn.Linear(d_model, 1), # Add the head layer to the model
|
||||
)
|
||||
```
|
||||
Explanation:
|
||||
In this implementation, the CustomTransformerModel class inherits from the TransformerModel class in qlib.contrib.model.pytorch_transformer. We override the __init__ method to add the additional MLP layers to the existing self.model. The forward method from the parent class will automatically apply the updated self.model layers, including the head layer.
|
||||
Modified config:
|
||||
```yaml
|
||||
model:
|
||||
class: CustomTransformerModel
|
||||
module_path: path.to.your.custom_transformer_model_module
|
||||
kwargs:
|
||||
d_feat: 16
|
||||
n_head: 8
|
||||
num_layers: 6
|
||||
d_model: 64
|
||||
d_ff: 2048
|
||||
dropout: 0.1
|
||||
```
|
||||
|
||||
ImplementActionTask_user:
|
||||
Dataset:
|
||||
target_component : |-
|
||||
Dataset
|
||||
DataHandler:
|
||||
target_component : |-
|
||||
DataHandler
|
||||
Model:
|
||||
target_component : |-
|
||||
Model
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import abc
|
||||
import re
|
||||
import subprocess
|
||||
import platform
|
||||
import inspect
|
||||
|
||||
from qlib.finco.llm import APIBackend
|
||||
from qlib.finco.tpl import get_tpl_path
|
||||
@@ -55,13 +56,16 @@ class Task:
|
||||
"""then all tasks can use this context manager to share the same context"""
|
||||
self._context_manager = context_manager
|
||||
|
||||
def save_chat_history_to_context_manager(self, user_input, response, system_prompt):
|
||||
def save_chat_history_to_context_manager(self, user_input, response, system_prompt, target_component="None"):
|
||||
chat_history = self._context_manager.get_context("chat_history")
|
||||
if chat_history is None:
|
||||
chat_history = []
|
||||
chat_history.append({"role": "system", "content": system_prompt})
|
||||
chat_history.append({"role": "user", "content": user_input})
|
||||
chat_history.append({"role": "assistant", "content": response})
|
||||
chat_history = {}
|
||||
if self.__class__.__name__ not in chat_history:
|
||||
chat_history[self.__class__.__name__] = {}
|
||||
if target_component not in chat_history[self.__class__.__name__]:
|
||||
chat_history[self.__class__.__name__][target_component] = [{"role": "system", "content": system_prompt}]
|
||||
chat_history[self.__class__.__name__][target_component].append({"role": "user", "content": user_input})
|
||||
chat_history[self.__class__.__name__][target_component].append({"role": "assistant", "content": response})
|
||||
self._context_manager.update_context("chat_history", chat_history)
|
||||
|
||||
@abc.abstractclassmethod
|
||||
@@ -147,8 +151,10 @@ class PlanTask(Task):
|
||||
|
||||
|
||||
class SLPlanTask(PlanTask):
|
||||
def __init__(self, ) -> None:
|
||||
def __init__(self, replan=False, error=None) -> None:
|
||||
super().__init__()
|
||||
self.replan = replan
|
||||
self.error = error
|
||||
|
||||
def execute(self):
|
||||
workflow = self._context_manager.get_context("workflow")
|
||||
@@ -157,25 +163,23 @@ class SLPlanTask(PlanTask):
|
||||
user_prompt = self._context_manager.get_context("user_prompt")
|
||||
assert user_prompt is not None, "The user prompt is not provided"
|
||||
prompt_plan_all = self.user.render(user_prompt=user_prompt)
|
||||
former_messages = []
|
||||
if self.replan:
|
||||
prompt_plan_all = f"your choice of predefined classes cannot be initialized.\nPlease rewrite the plan and answer with exact required format in system prompt and reply with no more explainations.\nThe error message: {self.error}. Please correct the former answer accordingly."
|
||||
former_messages = self._context_manager.get_context("chat_history")[self.__class__.__name__]['None'][1:]
|
||||
response = APIBackend().build_messages_and_create_chat_completion(
|
||||
prompt_plan_all, self.system.render()
|
||||
prompt_plan_all, self.system.render(), former_messages=former_messages
|
||||
)
|
||||
self.save_chat_history_to_context_manager(
|
||||
prompt_plan_all, response, self.system.render()
|
||||
)
|
||||
if "components" not in response:
|
||||
if "components" not in response.lower():
|
||||
self.logger.warning(
|
||||
"The response is not in the correct format, which probably means the answer is not correct"
|
||||
)
|
||||
|
||||
regex_dict = {
|
||||
"Dataset": re.compile("Dataset: \((.*?)\) (.*?)\n"),
|
||||
"DataHandler": re.compile("DataHandler: \((.*?)\) (.*?)\n"),
|
||||
"Model": re.compile("Model: \((.*?)\) (.*?)\n"),
|
||||
"Record": re.compile("Record: \((.*?)\) (.*?)\n"),
|
||||
"Strategy": re.compile("Strategy: \((.*?)\) (.*?)\n"),
|
||||
"Backtest": re.compile("Backtest: \((.*?)\) (.*?)$"),
|
||||
}
|
||||
|
||||
decision_pattern = re.compile("\((.*?)\)")
|
||||
class_pattern = re.compile("{(.*?)}-{(.*?)}")
|
||||
new_task = []
|
||||
# 1) create a workspace
|
||||
# TODO: we have to make choice between `sl` and `sl-cfg`
|
||||
@@ -186,18 +190,31 @@ class SLPlanTask(PlanTask):
|
||||
)
|
||||
|
||||
# 2) CURD on the workspace
|
||||
for name, regex in regex_dict.items():
|
||||
res = re.search(regex, response)
|
||||
if not res:
|
||||
self.logger.error(f"The search for {name} decision failed")
|
||||
else:
|
||||
self._context_manager.set_context(f"{name}_decision", res.group(1))
|
||||
self._context_manager.set_context(f"{name}_plan", res.group(2))
|
||||
assert res.group(1) in ["Default", "Personized"]
|
||||
if res.group(1) == "Default":
|
||||
new_task.append(ConfigActionTask(name))
|
||||
elif res.group(1) == "Personized":
|
||||
new_task.extend([ConfigActionTask(name), ImplementActionTask(name)])
|
||||
for name in COMPONENT_LIST:
|
||||
target_line = [line for line in response.split("\n") if name in line]
|
||||
assert len(target_line) == 1, f"The {name} is not found in the response"
|
||||
target_line = target_line[0].strip("- ")
|
||||
decision = re.search(decision_pattern, target_line)
|
||||
assert decision is not None, f"The decision of {name} is not found"
|
||||
decision = decision.group(1)
|
||||
classes = re.findall(class_pattern, target_line)
|
||||
try:
|
||||
for module_path, class_name in classes:
|
||||
exec(f"from {module_path} import {class_name}")
|
||||
except ImportError as e:
|
||||
self.logger.warning(f"The {class_name} is not found in {module_path}")
|
||||
return [SLPlanTask(replan=True, error=str(e))]
|
||||
self._context_manager.set_context(f"{name}_decision", decision)
|
||||
self._context_manager.set_context(f"{name}_classes", classes)
|
||||
self._context_manager.set_context(f"{name}_plan", target_line)
|
||||
|
||||
assert decision in ["Default", "Personized"], f"The decision of {name} is not correct"
|
||||
if decision == "Default":
|
||||
new_task.extend([HyperparameterActionTask(name), ConfigActionTask(name), YamlEditTask(name)])
|
||||
elif decision == "Personized":
|
||||
# TODO open ImplementActionTask to let GPT write code
|
||||
new_task.extend([HyperparameterActionTask(name), ConfigActionTask(name), YamlEditTask(name)])
|
||||
# new_task.extend([HyperparameterActionTask(name), ConfigActionTask(name), ImplementActionTask(name), CodeDumpTask(name), YamlEditTask(name)])
|
||||
return new_task
|
||||
|
||||
|
||||
@@ -385,53 +402,68 @@ class DifferentiatedComponentActionTask(ActionTask):
|
||||
return self.prompt_template.__getattribute__(self.__class__.__name__ + "_user_" + self.target_component)
|
||||
|
||||
|
||||
class ConfigActionTask(DifferentiatedComponentActionTask):
|
||||
def __init__(self, component) -> None:
|
||||
class HyperparameterActionTask(ActionTask):
|
||||
def __init__(self, component, regenerate=False, error=None) -> None:
|
||||
super().__init__()
|
||||
self.target_component = component
|
||||
self.regenerate = regenerate
|
||||
self.error = error
|
||||
|
||||
def execute(self):
|
||||
user_prompt = self._context_manager.get_context("user_prompt")
|
||||
prompt_element_dict = dict()
|
||||
for component in COMPONENT_LIST:
|
||||
prompt_element_dict[
|
||||
f"{component}_decision"
|
||||
] = self._context_manager.get_context(f"{component}_decision")
|
||||
prompt_element_dict[
|
||||
f"{component}_plan"
|
||||
] = self._context_manager.get_context(f"{component}_plan")
|
||||
|
||||
assert (
|
||||
None not in prompt_element_dict.values()
|
||||
), "Some decision or plan is not set by plan maker"
|
||||
target_component_decision = self._context_manager.get_context(f"{self.target_component}_decision")
|
||||
target_component_plan = self._context_manager.get_context(f"{self.target_component}_plan")
|
||||
target_component_classes = self._context_manager.get_context(f"{self.target_component}_classes")
|
||||
|
||||
config_prompt = self.user.render(
|
||||
for module_path, class_name in target_component_classes:
|
||||
exec(f"from {module_path} import {class_name}")
|
||||
|
||||
assert target_component_decision is not None, "target component decision is not set by plan maker"
|
||||
assert target_component_plan is not None, "target component plan is not set by plan maker"
|
||||
assert target_component_classes is not None, "target component classes is not set by plan maker"
|
||||
|
||||
system_prompt = self.system.render(target_module=self.target_component, choice=target_component_decision, classes=target_component_classes)
|
||||
|
||||
target_component_classes_and_hyperparameters = []
|
||||
for module_path, class_name in target_component_classes:
|
||||
exec(f"from {module_path} import {class_name}")
|
||||
hyperparameters = [hyperparameter for hyperparameter in {name: param for name, param in inspect.signature(eval(class_name).__init__).parameters.items() if name != "self" and name != "kwargs"}.keys()]
|
||||
if class_name == "LGBModel":
|
||||
hyperparameters.extend([ "boosting_type", "num_leaves", "max_depth", "learning_rate", "n_estimators", "objective", "class_weight", "min_split_gain", "min_child_weight", "min_child_samples", "subsample", "subsample_freq", "colsample_bytree", "reg_alpha", "reg_lambda", "random_state", "n_jobs", "silent", "importance_type", "early_stopping_round", "metric", "num_class", "is_unbalance", "bagging_seed", "verbosity", ])
|
||||
elif class_name == "SignalRecord":
|
||||
hyperparameters.remove("model")
|
||||
hyperparameters.remove("dataset")
|
||||
hyperparameters.remove("recorder")
|
||||
target_component_classes_and_hyperparameters.append((module_path, class_name, hyperparameters))
|
||||
user_prompt = self.user.render(
|
||||
user_requirement=user_prompt,
|
||||
decision=prompt_element_dict[f"{self.target_component}_decision"],
|
||||
plan=prompt_element_dict[f"{self.target_component}_plan"],
|
||||
target_component_plan=target_component_plan,
|
||||
target_component=self.target_component,
|
||||
target_component_classes_and_hyperparameters=target_component_classes_and_hyperparameters
|
||||
)
|
||||
former_messages = []
|
||||
if self.regenerate:
|
||||
user_prompt = f"your hyperparameter cannot be initialized, may be caused by wrong format of the value or wrong name or some value is not supported in Qlib.\nPlease rewrite the hyperparameters and answer with exact required format in system prompt and reply with no more explainations.\nThe error message: {self.error}. Please correct the former answer accordingly.\nHyperparameters, Reason and Improve suggestion should always be included."
|
||||
former_messages = self._context_manager.get_context("chat_history")[self.__class__.__name__][self.target_component][1:]
|
||||
response = APIBackend().build_messages_and_create_chat_completion(
|
||||
config_prompt, self.system.render()
|
||||
user_prompt, system_prompt, former_messages=former_messages
|
||||
)
|
||||
self.save_chat_history_to_context_manager(
|
||||
config_prompt, response, self.system.render()
|
||||
user_prompt, response, system_prompt, self.target_component
|
||||
)
|
||||
res = re.search(
|
||||
r"Config:(.*)Reason:(.*)Improve suggestion:(.*)", response, re.S
|
||||
r"(?i)Hyperparameters:(.*)Reason:(.*)Improve suggestion:(.*)", response, re.S
|
||||
)
|
||||
assert (
|
||||
res is not None and len(res.groups()) == 3
|
||||
), "The response of config action task is not in the correct format"
|
||||
|
||||
config = re.search(r"```yaml(.*)```", res.group(1), re.S)
|
||||
assert (
|
||||
config is not None
|
||||
), "The config part of config action task response is not in the correct format"
|
||||
config = config.group(1)
|
||||
hyperparameters = res.group(1)
|
||||
reason = res.group(2)
|
||||
improve_suggestion = res.group(3)
|
||||
|
||||
self._context_manager.set_context(f"{self.target_component}_config", config)
|
||||
self._context_manager.set_context(f"{self.target_component}_hyperparameters", hyperparameters)
|
||||
self._context_manager.set_context(f"{self.target_component}_reason", reason)
|
||||
self._context_manager.set_context(
|
||||
f"{self.target_component}_improve_suggestion", improve_suggestion
|
||||
@@ -439,11 +471,83 @@ class ConfigActionTask(DifferentiatedComponentActionTask):
|
||||
return []
|
||||
|
||||
|
||||
class ConfigActionTask(ActionTask):
|
||||
def __init__(self, component, reconfig=False, error=None) -> None:
|
||||
super().__init__()
|
||||
self.target_component = component
|
||||
self.reconfig = reconfig
|
||||
self.error = error
|
||||
|
||||
def execute(self):
|
||||
user_prompt = self._context_manager.get_context("user_prompt")
|
||||
|
||||
target_component_decision = self._context_manager.get_context(f"{self.target_component}_decision")
|
||||
target_component_plan = self._context_manager.get_context(f"{self.target_component}_plan")
|
||||
target_component_classes = self._context_manager.get_context(f"{self.target_component}_classes")
|
||||
target_component_hyperparameters = self._context_manager.get_context(f"{self.target_component}_hyperparameters")
|
||||
|
||||
system_prompt = self.system.render(target_module=self.target_component, choice=target_component_decision, classes=target_component_classes)
|
||||
user_prompt = self.user.render(
|
||||
user_requirement=user_prompt,
|
||||
target_component_plan=target_component_plan,
|
||||
target_component=self.target_component,
|
||||
target_component_hyperparameters=target_component_hyperparameters
|
||||
)
|
||||
former_messages = []
|
||||
if self.reconfig and user_prompt == self._context_manager.get_context("chat_history")[self.__class__.__name__][self.target_component][-2]["content"]:
|
||||
user_prompt = f"your config cannot be converted to YAML, may be caused by wrong format. Please rewrite the yaml and answer with exact required format in system prompt and reply with no more explainations.\nerror message: {self.error}\n"
|
||||
former_messages = self._context_manager.get_context("chat_history")[self.__class__.__name__][self.target_component][1:]
|
||||
response = APIBackend().build_messages_and_create_chat_completion(
|
||||
user_prompt, system_prompt, former_messages=former_messages
|
||||
)
|
||||
self.save_chat_history_to_context_manager(
|
||||
user_prompt, response, system_prompt, self.target_component
|
||||
)
|
||||
config = re.search(r"```yaml(.*)```", response, re.S).group(1)
|
||||
|
||||
try:
|
||||
yaml_config = yaml.safe_load(io.StringIO(config))
|
||||
except yaml.YAMLError as e:
|
||||
self.logger.info(f"Yaml file is not in the correct format: {e}")
|
||||
return_tasks = [HyperparameterActionTask(self.target_component, regenerate=True, error=str(e)), ConfigActionTask(self.target_component, reconfig=True, error=str(e))]
|
||||
return return_tasks
|
||||
|
||||
if self.target_component == "DataHandler":
|
||||
for processor in yaml_config['handler']['kwargs']['infer_processors']:
|
||||
if "kwargs" in processor and "fields_group" in processor["kwargs"]:
|
||||
del processor["kwargs"]['fields_group']
|
||||
for processor in yaml_config['handler']['kwargs']['learn_processors']:
|
||||
if "kwargs" in processor and "fields_group" in processor["kwargs"]:
|
||||
del processor["kwargs"]['fields_group']
|
||||
|
||||
if 'freq' in yaml_config['handler']['kwargs'] and yaml_config['handler']['kwargs']['freq'] == '1d':
|
||||
yaml_config['handler']['kwargs']['freq'] = "day"
|
||||
|
||||
def remove_default(config):
|
||||
if isinstance(config, dict):
|
||||
for key in list(config.keys()):
|
||||
if isinstance(config[key], str):
|
||||
if config[key].lower() == "default":
|
||||
del config[key]
|
||||
else:
|
||||
remove_default(config[key])
|
||||
elif isinstance(config, list):
|
||||
for item in config:
|
||||
remove_default(item)
|
||||
remove_default(yaml_config)
|
||||
|
||||
self._context_manager.set_context(f"{self.target_component}_config", yaml_config)
|
||||
return []
|
||||
|
||||
|
||||
|
||||
|
||||
class ImplementActionTask(DifferentiatedComponentActionTask):
|
||||
def __init__(self, target_component) -> None:
|
||||
def __init__(self, target_component, reimplement=False) -> None:
|
||||
super().__init__()
|
||||
self.target_component = target_component
|
||||
assert COMPONENT_LIST.index(self.target_component) <= 2, "The target component is not in dataset datahandler and model"
|
||||
self.reimplement = reimplement
|
||||
|
||||
def execute(self):
|
||||
"""
|
||||
@@ -472,11 +576,15 @@ class ImplementActionTask(DifferentiatedComponentActionTask):
|
||||
plan=prompt_element_dict[f"{self.target_component}_plan"],
|
||||
user_config=config,
|
||||
)
|
||||
former_messages = []
|
||||
if self.reimplement:
|
||||
implement_prompt = "your code seems wrong, please re-implement it and answer with exact required format and reply with no more explainations.\n"
|
||||
former_messages = self._context_manager.get_context("chat_history")[self.__class__.__name__][self.target_component][1:]
|
||||
response = APIBackend().build_messages_and_create_chat_completion(
|
||||
implement_prompt, self.system.render()
|
||||
implement_prompt, self.system.render(), former_messages=former_messages
|
||||
)
|
||||
self.save_chat_history_to_context_manager(
|
||||
implement_prompt, response, self.system.render()
|
||||
implement_prompt, response, self.system.render(), self.target_component
|
||||
)
|
||||
|
||||
res = re.search(
|
||||
@@ -512,7 +620,7 @@ class ImplementActionTask(DifferentiatedComponentActionTask):
|
||||
class YamlEditTask(ActionTask):
|
||||
"""This yaml edit task will replace a specific component directly"""
|
||||
|
||||
def __init__(self, file: Union[str, Path], module_path: str, updated_content: str):
|
||||
def __init__(self, target_component: str):
|
||||
"""
|
||||
|
||||
Parameters
|
||||
@@ -524,28 +632,93 @@ class YamlEditTask(ActionTask):
|
||||
updated_content
|
||||
The content to replace the original content in `module_path`
|
||||
"""
|
||||
self.p = Path(file)
|
||||
self.module_path = module_path
|
||||
self.updated_content = updated_content
|
||||
super().__init__()
|
||||
self.target_component = target_component
|
||||
self.target_config_key = {
|
||||
"Dataset": "dataset",
|
||||
"DataHandler": "handler",
|
||||
"Model": "model",
|
||||
"Strategy": "strategy",
|
||||
"Record": "record",
|
||||
"Backtest": "backtest",
|
||||
}[self.target_component]
|
||||
|
||||
def replace_key_value_recursive(self, target_dict, target_key, new_value):
|
||||
res = False
|
||||
if isinstance(target_dict, dict):
|
||||
for key, value in target_dict.items():
|
||||
if key == target_key:
|
||||
target_dict[key] = new_value
|
||||
res = True
|
||||
else:
|
||||
res = res | self.replace_key_value_recursive(value, target_key, new_value)
|
||||
elif isinstance(target_dict, list):
|
||||
for item in target_dict:
|
||||
res = res | self.replace_key_value_recursive(item, target_key, new_value)
|
||||
return res
|
||||
|
||||
|
||||
def execute(self):
|
||||
# 1) read original and new content
|
||||
with self.p.open("r") as f:
|
||||
config = yaml.safe_load(f)
|
||||
update_config = yaml.safe_load(io.StringIO(self.updated_content))
|
||||
self.original_config_location = Path(os.path.join(self._context_manager.get_context('workspace'), "workflow_config.yaml"))
|
||||
with self.original_config_location.open("r") as f:
|
||||
target_config = yaml.safe_load(f)
|
||||
update_config = self._context_manager.get_context(f'{self.target_component}_modified_config')
|
||||
if update_config is None:
|
||||
update_config = self._context_manager.get_context(f'{self.target_component}_config')
|
||||
|
||||
# 2) modify the module_path if code is implemented by finco
|
||||
# TODO because we skip code writing part, so we mute this step to avoid error
|
||||
# if self._context_manager.get_context(f'{self.target_component}_decision') == "Personized":
|
||||
# workspace = self._context_manager.get_context(f'workspace').name
|
||||
# module_path = f"qlib.finco.{workspace}.{self.target_component}_code"
|
||||
# if "module_path" in update_config[self.target_config_key]:
|
||||
# update_config[self.target_config_key]["module_path"] = module_path
|
||||
|
||||
# 2) locate the module
|
||||
focus = config
|
||||
module_list = self.module_path.split(".")
|
||||
for k in module_list[:-1]:
|
||||
focus = focus[k]
|
||||
# TODO here's small trick, we only update dataset and datahandler in the whole, so we skip dataset update and update both of them when datahandler is set. Because model may not set datahandler in dataset config, then we may not find 'handler' in the new config, this trick can be updated in the future.
|
||||
if self.target_component == "Dataset":
|
||||
return []
|
||||
elif self.target_component == "DataHandler":
|
||||
dataset_update_config = self._context_manager.get_context(f'Dataset_modified_config')
|
||||
if dataset_update_config is None:
|
||||
dataset_update_config = self._context_manager.get_context(f'Dataset_config')
|
||||
dataset_update_config['dataset']['kwargs']['handler'] = update_config['handler']
|
||||
update_config = dataset_update_config
|
||||
real_target_config_key = "dataset"
|
||||
else:
|
||||
real_target_config_key = self.target_config_key
|
||||
|
||||
# 3) replace the module and save
|
||||
focus[module_list[-1]] = update_config
|
||||
with self.p.open("w") as f:
|
||||
yaml.dump(config, f)
|
||||
|
||||
|
||||
# 3) replace the module
|
||||
assert isinstance(update_config, dict) and real_target_config_key in update_config, "The config file is not in the correct format"
|
||||
assert self.replace_key_value_recursive(target_config, real_target_config_key, update_config[real_target_config_key]), "Replace of the yaml file failed."
|
||||
|
||||
|
||||
# 4) save the config file
|
||||
with self.original_config_location.open("w") as f:
|
||||
yaml.dump(target_config, f)
|
||||
|
||||
return []
|
||||
|
||||
class CodeDumpTask(ActionTask):
|
||||
def __init__(self, target_component) -> None:
|
||||
super().__init__()
|
||||
self.target_component = target_component
|
||||
|
||||
def execute(self):
|
||||
code = self._context_manager.get_context(f'{self.target_component}_code')
|
||||
assert code is not None, "The code is not set"
|
||||
|
||||
with open(os.path.join(self._context_manager.get_context('workspace'), f'{self.target_component}_code.py'), 'w') as f:
|
||||
f.write(code)
|
||||
|
||||
try:
|
||||
exec(f"from qlib.finco.{os.path.basename(self._context_manager.get_context('workspace'))}.{self.target_component}_code import *")
|
||||
except (ImportError, AttributeError, SyntaxError):
|
||||
return [ImplementActionTask(self.target_component, reimplement=True), CodeDumpTask(self.target_component)]
|
||||
return []
|
||||
|
||||
class SummarizeTask(Task):
|
||||
__DEFAULT_WORKSPACE = "./"
|
||||
|
||||
@@ -582,12 +755,15 @@ class SummarizeTask(Task):
|
||||
|
||||
# todo: remove 'be' after test
|
||||
be = APIBackend()
|
||||
bak_debug_mode = be.debug_mode
|
||||
be.debug_mode = False
|
||||
response = be.build_messages_and_create_chat_completion(
|
||||
user_prompt=prompt_workflow_selection, system_prompt=self.system.render()
|
||||
)
|
||||
|
||||
self._context_manager.set_context("summary", response)
|
||||
be.debug_mode = bak_debug_mode
|
||||
|
||||
self.save_markdown(content=response)
|
||||
self.logger.info(f"Report has saved to {self.__DEFAULT_REPORT_NAME}", title="End")
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from qlib.finco.prompt_template import PromptTemplate, Template
|
||||
from qlib.finco.log import FinCoLog, LogColors
|
||||
from qlib.finco.utils import similarity
|
||||
from qlib.finco.llm import APIBackend
|
||||
from qlib.finco.conf import Config
|
||||
|
||||
|
||||
class WorkflowContextManager:
|
||||
@@ -71,16 +72,17 @@ class WorkflowManager:
|
||||
self._workspace = Path.cwd() / "finco_workspace"
|
||||
else:
|
||||
self._workspace = Path(workspace)
|
||||
self.conf = Config()
|
||||
self._confirm_and_rm()
|
||||
|
||||
self.prompt_template = PromptTemplate()
|
||||
self.context = WorkflowContextManager()
|
||||
self.context.set_context("workspace", self._workspace)
|
||||
self.default_user_prompt = "Please help me build a low turnover strategy that focus more on longterm return in China a stock market. Please help to pick one third of the factors in Alpha360 and use lightGBM model."
|
||||
self.default_user_prompt = "Please help me build a low turnover strategy that focus more on longterm return in China A csi800. Please help to use lightgbm model."
|
||||
|
||||
def _confirm_and_rm(self):
|
||||
# if workspace exists, please confirm and remove it. Otherwise exit.
|
||||
if self._workspace.exists():
|
||||
if self._workspace.exists() and not self.conf.continuous_mode:
|
||||
self.logger.info(title="Interact")
|
||||
flag = input(
|
||||
LogColors().render(
|
||||
@@ -95,6 +97,8 @@ class WorkflowManager:
|
||||
else:
|
||||
# remove self._workspace
|
||||
shutil.rmtree(self._workspace)
|
||||
elif self._workspace.exists() and self.conf.continuous_mode:
|
||||
shutil.rmtree(self._workspace)
|
||||
|
||||
def set_context(self, key, value):
|
||||
"""Direct call set_context method of the context manager"""
|
||||
|
||||
Reference in New Issue
Block a user